diff --git a/app/Http/Controllers/Admin/Nodes/NodeController.php b/app/Http/Controllers/Admin/Nodes/NodeController.php new file mode 100644 index 000000000..5e285a8c6 --- /dev/null +++ b/app/Http/Controllers/Admin/Nodes/NodeController.php @@ -0,0 +1,48 @@ +view = $view; + $this->repository = $repository; + } + + /** + * Returns a listing of nodes on the system. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\View\View + */ + public function index(Request $request) + { + $nodes = $this->repository + ->setSearchTerm($request->input('query')) + ->getNodeListingData(); + + return $this->view->make('admin.nodes.index', compact('nodes')); + } +} diff --git a/app/Http/Controllers/Admin/Nodes/NodeViewController.php b/app/Http/Controllers/Admin/Nodes/NodeViewController.php new file mode 100644 index 000000000..017706351 --- /dev/null +++ b/app/Http/Controllers/Admin/Nodes/NodeViewController.php @@ -0,0 +1,160 @@ +repository = $repository; + $this->view = $view; + $this->versionService = $versionService; + $this->locationRepository = $locationRepository; + $this->allocationRepository = $allocationRepository; + $this->serverRepository = $serverRepository; + } + + /** + * Returns index view for a specific node on the system. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Node $node + * @return \Illuminate\Contracts\View\View + */ + public function index(Request $request, Node $node) + { + $node = $this->repository->loadLocationAndServerCount($node); + + return $this->view->make('admin.nodes.view.index', [ + 'node' => $node, + 'stats' => $this->repository->getUsageStats($node), + 'version' => $this->versionService, + ]); + } + + /** + * Returns the settings page for a specific node. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Node $node + * @return \Illuminate\Contracts\View\View + */ + public function settings(Request $request, Node $node) + { + return $this->view->make('admin.nodes.view.settings', [ + 'node' => $node, + 'locations' => $this->locationRepository->all(), + ]); + } + + /** + * Return the node configuration page for a specific node. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Node $node + * @return \Illuminate\Contracts\View\View + */ + public function configuration(Request $request, Node $node) + { + return $this->view->make('admin.nodes.view.configuration', compact('node')); + } + + /** + * Return the node allocation management page. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Node $node + * @return \Illuminate\Contracts\View\View + */ + public function allocations(Request $request, Node $node) + { + $node = $this->repository->loadNodeAllocations($node); + + $this->plainInject(['node' => Collection::wrap($node)->only(['id'])]); + + return $this->view->make('admin.nodes.view.allocation', [ + 'node' => $node, + 'allocations' => $this->allocationRepository->setColumns(['ip'])->getUniqueAllocationIpsForNode($node->id), + ]); + } + + /** + * Return a listing of servers that exist for this specific node. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Node $node + * @return \Illuminate\Contracts\View\View + */ + public function servers(Request $request, Node $node) + { + $this->plainInject([ + 'node' => Collection::wrap($node->makeVisible('daemonSecret')) + ->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), + ]); + + return $this->view->make('admin.nodes.view.servers', [ + 'node' => $node, + 'servers' => $this->serverRepository->loadAllServersForNode($node->id, 25), + ]); + } +} diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 8d4048d21..a7d710a10 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -9,7 +9,6 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Javascript; use Cake\Chronos\Chronos; use Illuminate\Http\Request; use Pterodactyl\Models\Node; @@ -138,19 +137,6 @@ class NodesController extends Controller $this->versionService = $versionService; } - /** - * Displays the index page listing all nodes on the panel. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - */ - public function index(Request $request) - { - return view('admin.nodes.index', [ - 'nodes' => $this->repository->setSearchTerm($request->input('query'))->getNodeListingData(), - ]); - } - /** * Displays create new node page. * @@ -184,79 +170,6 @@ class NodesController extends Controller return redirect()->route('admin.nodes.view.allocation', $node->id); } - /** - * Shows the index overview page for a specific node. - * - * @param \Pterodactyl\Models\Node $node - * @return \Illuminate\View\View - */ - public function viewIndex(Node $node) - { - return view('admin.nodes.view.index', [ - 'node' => $this->repository->loadLocationAndServerCount($node), - 'stats' => $this->repository->getUsageStats($node), - 'version' => $this->versionService, - ]); - } - - /** - * Shows the settings page for a specific node. - * - * @param \Pterodactyl\Models\Node $node - * @return \Illuminate\View\View - */ - public function viewSettings(Node $node) - { - return view('admin.nodes.view.settings', [ - 'node' => $node, - 'locations' => $this->locationRepository->all(), - ]); - } - - /** - * Shows the configuration page for a specific node. - * - * @param \Pterodactyl\Models\Node $node - * @return \Illuminate\View\View - */ - public function viewConfiguration(Node $node) - { - return view('admin.nodes.view.configuration', ['node' => $node]); - } - - /** - * Shows the allocation page for a specific node. - * - * @param \Pterodactyl\Models\Node $node - * @return \Illuminate\View\View - */ - public function viewAllocation(Node $node) - { - $this->repository->loadNodeAllocations($node); - Javascript::put(['node' => collect($node)->only(['id'])]); - - return view('admin.nodes.view.allocation', [ - 'allocations' => $this->allocationRepository->setColumns(['ip'])->getUniqueAllocationIpsForNode($node->id), - 'node' => $node, - ]); - } - - /** - * Shows the server listing page for a specific node. - * - * @param \Pterodactyl\Models\Node $node - * @return \Illuminate\View\View - */ - public function viewServers(Node $node) - { - $servers = $this->serverRepository->loadAllServersForNode($node->id, 25); - Javascript::put([ - 'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), - ]); - - return view('admin.nodes.view.servers', ['node' => $node, 'servers' => $servers]); - } - /** * Updates settings for a node. * diff --git a/app/Models/Server.php b/app/Models/Server.php index 68f3e8441..d6a343c1c 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -170,6 +170,18 @@ class Server extends Validable return Schema::getColumnListing($this->getTable()); } + /** + * Returns the format for server allocations when communicating with the Daemon. + * + * @return array + */ + public function getAllocationMappings(): array + { + return $this->allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(); + } + /** * @return bool */ diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index a4d57354e..89abbeeb3 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -31,27 +31,31 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa */ public function getUsageStats(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(); + $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 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 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; + $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 [ + $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(); } /** @@ -132,7 +136,12 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa 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', 'asc')->with('server:id,name')->paginate(50) + $node->allocations() + ->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL') + ->orderByRaw('INET_ATON(ip) ASC') + ->orderBy('port', 'asc') + ->with('server:id,name') + ->paginate(50) ); return $node; diff --git a/app/Repositories/Wings/DaemonServerRepository.php b/app/Repositories/Wings/DaemonServerRepository.php index 502c7ddd2..9bcf80b84 100644 --- a/app/Repositories/Wings/DaemonServerRepository.php +++ b/app/Repositories/Wings/DaemonServerRepository.php @@ -51,4 +51,22 @@ class DaemonServerRepository extends DaemonRepository throw new DaemonConnectionException($exception); } } + + /** + * Updates details about a server on the Daemon. + * + * @param array $data + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function update(array $data): void + { + Assert::isInstanceOf($this->server, Server::class); + + try { + $this->getHttpClient()->patch('/api/servers/' . $this->server->uuid, ['json' => $data]); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception); + } + } } diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index 5db59eb2d..e8adabe48 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -6,10 +6,10 @@ use Pterodactyl\Models\Node; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; class NodeUpdateService { @@ -32,12 +32,12 @@ class NodeUpdateService * UpdateService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository + * @param \Pterodactyl\Repositories\Daemon\ConfigurationRepository $configurationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ public function __construct( ConnectionInterface $connection, - ConfigurationRepositoryInterface $configurationRepository, + ConfigurationRepository $configurationRepository, NodeRepositoryInterface $repository ) { $this->connection = $connection; @@ -58,6 +58,8 @@ class NodeUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException + * + * @throws \GuzzleHttp\Exception\GuzzleException */ public function handle(Node $node, array $data, bool $resetToken = false) { diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 67f6f98da..1bbd705e0 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -6,11 +6,11 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class BuildModificationService { @@ -25,7 +25,7 @@ class BuildModificationService private $connection; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ private $daemonServerRepository; @@ -39,13 +39,13 @@ class BuildModificationService * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ public function __construct( AllocationRepositoryInterface $allocationRepository, ConnectionInterface $connection, - DaemonServerRepositoryInterface $daemonServerRepository, + DaemonServerRepository $daemonServerRepository, ServerRepositoryInterface $repository ) { $this->allocationRepository = $allocationRepository; @@ -67,23 +67,22 @@ class BuildModificationService */ public function handle(Server $server, array $data) { - $build = []; $this->connection->beginTransaction(); $this->processAllocations($server, $data); + if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { try { - $allocation = $this->allocationRepository->findFirstWhere([ + $this->allocationRepository->findFirstWhere([ ['id', '=', $data['allocation_id']], ['server_id', '=', $server->id], ]); } catch (RecordNotFoundException $ex) { throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found')); } - - $build['default'] = ['ip' => $allocation->ip, 'port' => $allocation->port]; } + /** @var \Pterodactyl\Models\Server $server */ $server = $this->repository->withFreshModel()->update($server->id, [ 'oom_disabled' => array_get($data, 'oom_disabled'), 'memory' => array_get($data, 'memory'), @@ -96,20 +95,28 @@ class BuildModificationService 'allocation_limit' => array_get($data, 'allocation_limit'), ]); - $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); - - $build['oom_disabled'] = $server->oom_disabled; - $build['memory'] = (int) $server->memory; - $build['swap'] = (int) $server->swap; - $build['io'] = (int) $server->io; - $build['cpu'] = (int) $server->cpu; - $build['disk'] = (int) $server->disk; - $build['ports|overwrite'] = $allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(); + $updateData = [ + 'allocations' => [ + 'default' => [ + 'ip' => $server->allocation->ip, + 'port' => $server->allocation->port, + ], + 'mappings' => [$server->getAllocationMappings()], + ], + 'build' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'io' => $server->io, + 'cpu' => $server->cpu, + 'disk' => $server->disk, + ], + 'container' => [ + 'oom_disabled' => $server->oom_disabled, + ], + ]; try { - $this->daemonServerRepository->setServer($server)->update(['build' => $build]); + $this->daemonServerRepository->setServer($server)->update($updateData); $this->connection->commit(); } catch (RequestException $exception) { throw new DaemonConnectionException($exception); diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index c0465e6cb..5e57a3a4f 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -76,7 +76,6 @@ class ServerConfigurationStructureService 'suspended' => (bool) $server->suspended, 'environment' => $this->environment->handle($server), 'build' => [ - 'oom_disabled' => $server->oom_disabled, 'memory' => $server->memory, 'swap' => $server->swap, 'io' => $server->io, @@ -90,6 +89,7 @@ class ServerConfigurationStructureService ], 'container' => [ 'image' => $server->image, + 'oom_disabled' => $server->oom_disabled, 'requires_rebuild' => false, ], 'allocations' => [ @@ -97,11 +97,7 @@ class ServerConfigurationStructureService 'ip' => $server->allocation->ip, 'port' => $server->allocation->port, ], - 'mappings' => [ - $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - ], + 'mappings' => [$server->getAllocationMappings()], ], ]; } diff --git a/routes/admin.php b/routes/admin.php index e91b2a4e9..d36ec4468 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -143,13 +143,13 @@ Route::group(['prefix' => 'servers'], function () { | */ Route::group(['prefix' => 'nodes'], function () { - Route::get('/', 'NodesController@index')->name('admin.nodes'); + Route::get('/', 'Nodes\NodeController@index')->name('admin.nodes'); Route::get('/new', 'NodesController@create')->name('admin.nodes.new'); - Route::get('/view/{node}', 'NodesController@viewIndex')->name('admin.nodes.view'); - Route::get('/view/{node}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); - Route::get('/view/{node}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); - Route::get('/view/{node}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); - Route::get('/view/{node}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); + Route::get('/view/{node}', 'Nodes\NodeViewController@index')->name('admin.nodes.view'); + Route::get('/view/{node}/settings', 'Nodes\NodeViewController@settings')->name('admin.nodes.view.settings'); + Route::get('/view/{node}/configuration', 'Nodes\NodeViewController@configuration')->name('admin.nodes.view.configuration'); + Route::get('/view/{node}/allocation', 'Nodes\NodeViewController@allocations')->name('admin.nodes.view.allocation'); + Route::get('/view/{node}/servers', 'Nodes\NodeViewController@servers')->name('admin.nodes.view.servers'); Route::get('/view/{node}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token'); Route::post('/new', 'NodesController@store');