Replace node repository

This commit is contained in:
Lance Pioch 2022-10-23 22:17:24 -04:00
parent 7266c66ebf
commit aeb7590a6d
14 changed files with 101 additions and 250 deletions

View file

@ -1,38 +0,0 @@
<?php
namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
interface NodeRepositoryInterface extends RepositoryInterface
{
public const THRESHOLD_PERCENTAGE_LOW = 75;
public const THRESHOLD_PERCENTAGE_MEDIUM = 90;
/**
* Return the usage stats for a single node.
*/
public function getUsageStats(Node $node): array;
/**
* Return the usage stats for a single node.
*/
public function getUsageStatsRaw(Node $node): array;
/**
* Return a single node with location and server information.
*/
public function loadLocationAndServerCount(Node $node, bool $refresh = false): Node;
/**
* Attach a paginated set of allocations to a node mode including
* any servers that are also attached to those allocations.
*/
public function loadNodeAllocations(Node $node, bool $refresh = false): Node;
/**
* Return a collection of nodes for all locations to use in server creation UI.
*/
public function getNodesForServerCreation(): Collection;
}

View file

@ -9,7 +9,6 @@ use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
@ -26,7 +25,6 @@ class NodeViewController extends Controller
public function __construct(
private AllocationRepository $allocationRepository,
private LocationRepository $locationRepository,
private NodeRepository $repository,
private ServerRepository $serverRepository,
private SoftwareVersionService $versionService,
private ViewFactory $view
@ -38,11 +36,37 @@ class NodeViewController extends Controller
*/
public function index(Request $request, Node $node): View
{
$node = $this->repository->loadLocationAndServerCount($node);
$node->load('location')->loadCount('servers');
$stats = Node::query()
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->join('servers', 'servers.node_id', '=', 'nodes.id')
->where('node_id', '=', $node->id)
->first();
$usageStats = Collection::make(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])
->mapWithKeys(function ($value, $key) use ($node) {
$maxUsage = $node->{$key};
if ($node->{$key . '_overallocate'} > 0) {
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
}
$percent = ($value / $maxUsage) * 100;
return [
$key => [
'value' => number_format($value),
'max' => number_format($maxUsage),
'percent' => $percent,
'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'),
],
];
})
->toArray();
return $this->view->make('admin.nodes.view.index', [
'node' => $node,
'stats' => $this->repository->getUsageStats($node),
'stats' => $usageStats,
'version' => $this->versionService,
]);
}
@ -71,7 +95,15 @@ class NodeViewController extends Controller
*/
public function allocations(Request $request, Node $node): View
{
$node = $this->repository->loadNodeAllocations($node);
$node->setRelation(
'allocations',
$node->allocations()
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
->orderByRaw('INET_ATON(ip) ASC')
->orderBy('port')
->with('server:id,name')
->paginate(50)
);
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);

View file

@ -18,7 +18,6 @@ use Pterodactyl\Services\Nodes\NodeDeletionService;
use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
@ -40,7 +39,6 @@ class NodesController extends Controller
protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService,
protected LocationRepositoryInterface $locationRepository,
protected NodeRepositoryInterface $repository,
protected ServerRepositoryInterface $serverRepository,
protected NodeUpdateService $updateService,
protected SoftwareVersionService $versionService,

View file

@ -11,7 +11,6 @@ use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Http\Requests\Admin\ServerFormRequest;
use Pterodactyl\Services\Servers\ServerCreationService;
@ -23,7 +22,6 @@ class CreateServerController extends Controller
public function __construct(
private AlertsMessageBag $alert,
private NestRepository $nestRepository,
private NodeRepository $nodeRepository,
private ServerCreationService $creationService,
private ViewFactory $view
) {
@ -46,7 +44,7 @@ class CreateServerController extends Controller
$nests = $this->nestRepository->getWithEggs();
JavaScript::put([
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
'nodeData' => Node::getForServerCreation(),
'nests' => $nests->map(function ($item) {
return array_merge($item->toArray(), [
'eggs' => $item->eggs->keyBy('id')->toArray(),

View file

@ -3,13 +3,13 @@
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Http\RedirectResponse;
use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\TransferService;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
@ -21,7 +21,6 @@ class ServerTransferController extends Controller
public function __construct(
private AlertsMessageBag $alert,
private AllocationRepositoryInterface $allocationRepository,
private NodeRepository $nodeRepository,
private TransferService $transferService,
private DaemonConfigurationRepository $daemonConfigurationRepository
) {
@ -45,7 +44,13 @@ class ServerTransferController extends Controller
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = $this->nodeRepository->getNodeWithResourceUsage($node_id);
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();
if ($node->isViable($server->memory, $server->disk)) {
// Check if the selected daemon is online.
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();

View file

@ -6,13 +6,13 @@ use JavaScript;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\EnvironmentService;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\MountRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
@ -31,7 +31,6 @@ class ServerViewController extends Controller
private LocationRepository $locationRepository,
private MountRepository $mountRepository,
private NestRepository $nestRepository,
private NodeRepository $nodeRepository,
private ServerRepository $repository,
private EnvironmentService $environmentService,
private ViewFactory $view
@ -128,14 +127,14 @@ class ServerViewController extends Controller
}
// Check if the panel doesn't have at least 2 nodes configured.
$nodes = $this->nodeRepository->all();
$nodeCount = Node::query()->count();
$canTransfer = false;
if (count($nodes) >= 2) {
if ($nodeCount >= 2) {
$canTransfer = true;
}
JavaScript::put([
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
'nodeData' => Node::getForServerCreation(),
]);
return $this->view->make('admin.servers.view.manage', [

View file

@ -4,10 +4,9 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@ -23,14 +22,14 @@ class DaemonAuthenticate
/**
* DaemonAuthenticate constructor.
*/
public function __construct(private Encrypter $encrypter, private NodeRepository $repository)
public function __construct(private Encrypter $encrypter)
{
}
/**
* Check if a request from the daemon can be properly attributed back to a single node instance.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws HttpException
*/
public function handle(Request $request, Closure $next): mixed
{
@ -43,25 +42,19 @@ class DaemonAuthenticate
}
$parts = explode('.', $bearer);
// Ensure that all of the correct parts are provided in the header.
// Ensure that all the correct parts are provided in the header.
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
throw new BadRequestHttpException('The Authorization header provided was not in a valid format.');
}
try {
/** @var \Pterodactyl\Models\Node $node */
$node = $this->repository->findFirstWhere([
'daemon_token_id' => $parts[0],
]);
/** @var Node $node */
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
if (hash_equals((string) $this->encrypter->decrypt($node->daemon_token), $parts[1])) {
$request->attributes->set('node', $node);
return $next($request);
}
} catch (RecordNotFoundException $exception) {
// Do nothing, we don't want to expose a node not existing at all.
}
throw new AccessDeniedHttpException('You are not authorized to access this resource.');
}

View file

@ -225,4 +225,26 @@ class Node extends Model
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
}
public static function getForServerCreation()
{
return self::with('allocations')->get()->map(function (Node $item) {
$filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) {
return collect($map)->only(['id', 'ip', 'port']);
});
$item->ports = $filtered->map(function ($map) {
return [
'id' => $map['id'],
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
];
})->values();
return [
'id' => $item->id,
'text' => $item->name,
'allocations' => $item->ports,
];
})->values();
}
}

View file

@ -5,7 +5,6 @@ namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Repositories\Eloquent\UserRepository;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
@ -20,7 +19,6 @@ use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\EggVariableRepository;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
@ -54,7 +52,6 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(EggVariableRepositoryInterface::class, EggVariableRepository::class);
$this->app->bind(LocationRepositoryInterface::class, LocationRepository::class);
$this->app->bind(NestRepositoryInterface::class, NestRepository::class);
$this->app->bind(NodeRepositoryInterface::class, NodeRepository::class);
$this->app->bind(ScheduleRepositoryInterface::class, ScheduleRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);
$this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class);

View file

@ -3,10 +3,8 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeRepository extends EloquentRepository implements NodeRepositoryInterface
class NodeRepository
{
/**
* Return the model backing this repository.
@ -15,138 +13,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
{
return Node::class;
}
/**
* Return the usage stats for a single node.
*/
public function getUsageStats(Node $node): array
{
$stats = $this->getBuilder()
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->join('servers', 'servers.node_id', '=', 'nodes.id')
->where('node_id', '=', $node->id)
->first();
return Collection::make(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])
->mapWithKeys(function ($value, $key) use ($node) {
$maxUsage = $node->{$key};
if ($node->{$key . '_overallocate'} > 0) {
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
}
$percent = ($value / $maxUsage) * 100;
return [
$key => [
'value' => number_format($value),
'max' => number_format($maxUsage),
'percent' => $percent,
'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'),
],
];
})
->toArray();
}
/**
* Return the usage stats for a single node.
*/
public function getUsageStatsRaw(Node $node): array
{
$stats = $this->getBuilder()->select(
$this->getBuilder()->raw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
)->join('servers', 'servers.node_id', '=', 'nodes.id')->where('node_id', $node->id)->first();
return collect(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])->mapWithKeys(function ($value, $key) use ($node) {
$maxUsage = $node->{$key};
if ($node->{$key . '_overallocate'} > 0) {
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
}
return [
$key => [
'value' => $value,
'max' => $maxUsage,
],
];
})->toArray();
}
/**
* Return a single node with location and server information.
*/
public function loadLocationAndServerCount(Node $node, bool $refresh = false): Node
{
if (!$node->relationLoaded('location') || $refresh) {
$node->load('location');
}
// This is quite ugly and can probably be improved down the road.
// And by probably, I mean it should.
if (is_null($node->servers_count) || $refresh) {
$node->load('servers');
$node->setRelation('servers_count', count($node->getRelation('servers')));
unset($node->servers);
}
return $node;
}
/**
* Attach a paginated set of allocations to a node mode including
* any servers that are also attached to those allocations.
*/
public function loadNodeAllocations(Node $node, bool $refresh = false): Node
{
$node->setRelation(
'allocations',
$node->allocations()
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
->orderByRaw('INET_ATON(ip) ASC')
->orderBy('port')
->with('server:id,name')
->paginate(50)
);
return $node;
}
/**
* Return a collection of nodes for all locations to use in server creation UI.
*/
public function getNodesForServerCreation(): Collection
{
return $this->getBuilder()->with('allocations')->get()->map(function (Node $item) {
$filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) {
return collect($map)->only(['id', 'ip', 'port']);
});
$item->ports = $filtered->map(function ($map) {
return [
'id' => $map['id'],
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
];
})->values();
return [
'id' => $item->id,
'text' => $item->name,
'allocations' => $item->ports,
];
})->values();
}
/**
* Returns a node with the given id with the Node's resource usage.
*/
public function getNodeWithResourceUsage(int $node_id): Node
{
$instance = $this->getBuilder()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id);
return $instance->first();
}
}

View file

@ -2,39 +2,26 @@
namespace Pterodactyl\Services\Locations;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Location;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Location\HasActiveNodesException;
class LocationDeletionService
{
/**
* LocationDeletionService constructor.
*/
public function __construct(
protected LocationRepositoryInterface $repository,
protected NodeRepositoryInterface $nodeRepository
) {
}
/**
* Delete an existing location.
*
* @throws \Pterodactyl\Exceptions\Service\Location\HasActiveNodesException
* @throws HasActiveNodesException
*/
public function handle(Location|int $location): ?int
{
$location = ($location instanceof Location) ? $location->id : $location;
/** @var Location $location */
$location = ($location instanceof Location) ? $location : Location::query()->findOrFail($location);
Assert::integerish($location, 'First argument passed to handle must be numeric or an instance of ' . Location::class . ', received %s.');
$count = $this->nodeRepository->findCountWhere([['location_id', '=', $location]]);
$count = $location->nodes()->count();
if ($count > 0) {
throw new HasActiveNodesException(trans('exceptions.locations.has_nodes'));
}
return $this->repository->delete($location);
return $location->delete();
}
}

View file

@ -6,21 +6,19 @@ use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use Pterodactyl\Models\Node;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeCreationService
{
/**
* NodeCreationService constructor.
*/
public function __construct(private Encrypter $encrypter, protected NodeRepositoryInterface $repository)
public function __construct(private Encrypter $encrypter)
{
}
/**
* Create a new node on the panel.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(array $data): Node
{
@ -28,6 +26,9 @@ class NodeCreationService
$data['daemon_token'] = $this->encrypter->encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
return $this->repository->create($data, true, true);
/** @var Node $node */
$node = Node::query()->create($data);
return $node;
}
}

View file

@ -4,38 +4,33 @@ namespace Pterodactyl\Services\Nodes;
use Pterodactyl\Models\Node;
use Illuminate\Contracts\Translation\Translator;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class NodeDeletionService
{
/**
* NodeDeletionService constructor.
*/
public function __construct(
protected NodeRepositoryInterface $repository,
protected ServerRepositoryInterface $serverRepository,
protected Translator $translator
) {
public function __construct(protected Translator $translator)
{
}
/**
* Delete a node from the panel if no servers are attached to it.
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
* @throws HasActiveServersException
*/
public function handle(int|Node $node): int
{
if ($node instanceof Node) {
$node = $node->id;
if (is_int($node)) {
$node = Node::query()->findOrFail($node);
}
$servers = $this->serverRepository->setColumns('id')->findCountWhere([['node_id', '=', $node]]);
$servers = $node->servers()->count();
if ($servers > 0) {
throw new HasActiveServersException($this->translator->get('exceptions.node.servers_attached'));
}
return $this->repository->delete($node);
return $node->delete();
}
}

View file

@ -7,7 +7,6 @@ use Pterodactyl\Models\Node;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
@ -20,8 +19,7 @@ class NodeUpdateService
public function __construct(
private ConnectionInterface $connection,
private DaemonConfigurationRepository $configurationRepository,
private Encrypter $encrypter,
private NodeRepository $repository
private Encrypter $encrypter
) {
}
@ -39,7 +37,7 @@ class NodeUpdateService
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
/** @var \Pterodactyl\Models\Node $updated */
$updated = $this->repository->withFreshModel()->update($node->id, $data, true, true);
$updated = (clone $node)->update($data);
try {
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection