From ccda2b63faac8355fa3f7d59838c747a35d94afc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 19 Sep 2017 22:10:14 -0500 Subject: [PATCH] Add more CLI commands for panel management --- app/Console/Commands/AddNode.php | 110 ------------- app/Console/Commands/ClearTasks.php | 78 ---------- .../CleanServiceBackupFilesCommand.php} | 75 +++++---- app/Console/Commands/RebuildServer.php | 144 ------------------ app/Console/Commands/RunTasks.php | 79 ---------- .../Schedule/ProcessRunnableCommand.php | 25 ++- .../Commands/Server/RebuildServerCommand.php | 140 +++++++++++++++++ app/Console/Kernel.php | 9 +- .../Repository/ServerRepositoryInterface.php | 11 +- .../Eloquent/ServerRepository.php | 27 +++- app/Services/Servers/EnvironmentService.php | 8 +- resources/lang/en/command/messages.php | 6 + .../Unit/Commands/CommandTestCase.php | 59 +++---- .../CleanServiceBackupFilesCommandTest.php | 110 +++++++++++++ .../Schedule/ProcessRunnableCommandTest.php | 131 ++++++++++++++++ 15 files changed, 496 insertions(+), 516 deletions(-) delete mode 100644 app/Console/Commands/AddNode.php delete mode 100644 app/Console/Commands/ClearTasks.php rename app/Console/Commands/{ClearServices.php => Maintenance/CleanServiceBackupFilesCommand.php} (51%) delete mode 100644 app/Console/Commands/RebuildServer.php delete mode 100644 app/Console/Commands/RunTasks.php create mode 100644 app/Console/Commands/Server/RebuildServerCommand.php rename app/Console/Commands/CleanServiceBackup.php => tests/Unit/Commands/CommandTestCase.php (55%) create mode 100644 tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php create mode 100644 tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php diff --git a/app/Console/Commands/AddNode.php b/app/Console/Commands/AddNode.php deleted file mode 100644 index 9f31527e..00000000 --- a/app/Console/Commands/AddNode.php +++ /dev/null @@ -1,110 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Console\Commands; - -use Illuminate\Console\Command; -use Pterodactyl\Models\Location; -use Pterodactyl\Repositories\NodeRepository; - -class AddNode extends Command -{ - protected $data = []; - - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'pterodactyl:node - {--name= : Name of the node.} - {--location= : The shortcode of the location to add this node to.} - {--fqdn= : The fully-qualified domain for the node.} - {--ssl= : Should the daemon use SSL for connections (T/F).} - {--memory= : The total memory available on this node for servers.} - {--disk= : The total disk space available on this node for servers.} - {--daemonBase= : The directory in which server files will be stored.} - {--daemonListen= : The port the daemon will listen on for connections.} - {--daemonSFTP= : The port to be used for SFTP conncetions to the daemon.}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Adds a new node to the system via the CLI.'; - - /** - * Create a new command instance. - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $locations = Location::all(['id', 'short', 'long']); - - $this->data['name'] = (is_null($this->option('name'))) ? $this->ask('Node Name') : $this->option('name'); - - if (is_null($this->option('location'))) { - $this->table(['ID', 'Short Code', 'Description'], $locations->toArray()); - $selectedLocation = $this->anticipate('Node Location (Short Name)', $locations->pluck('short')->toArray()); - } else { - $selectedLocation = $this->option('location'); - } - - $this->data['location_id'] = $locations->where('short', $selectedLocation)->first()->id; - - if (is_null($this->option('fqdn'))) { - $this->line('Please enter domain name (e.g node.example.com) to be used for connecting to the daemon. An IP address may only be used if you are not using SSL for this node.'); - $this->data['fqdn'] = $this->ask('Fully Qualified Domain Name'); - } else { - $this->data['fqdn'] = $this->option('fqdn'); - } - - $useSSL = (is_null($this->option('ssl'))) ? $this->confirm('Use SSL', true) : $this->option('ssl'); - - $this->data['scheme'] = ($useSSL) ? 'https' : 'http'; - $this->data['memory'] = (is_null($this->option('memory'))) ? $this->ask('Total Memory (in MB)') : $this->option('memory'); - $this->data['memory_overallocate'] = 0; - $this->data['disk'] = (is_null($this->option('disk'))) ? $this->ask('Total Disk Space (in MB)') : $this->option('disk'); - $this->data['disk_overallocate'] = 0; - $this->data['public'] = 1; - $this->data['daemonBase'] = (is_null($this->option('daemonBase'))) ? $this->ask('Daemon Server File Location', '/srv/daemon-data') : $this->option('daemonBase'); - $this->data['daemonListen'] = (is_null($this->option('daemonListen'))) ? $this->ask('Daemon Listening Port', 8080) : $this->option('daemonListen'); - $this->data['daemonSFTP'] = (is_null($this->option('daemonSFTP'))) ? $this->ask('Daemon SFTP Port', 2022) : $this->option('daemonSFTP'); - - $repo = new NodeRepository; - $id = $repo->create($this->data); - - $this->info('Node created with ID: ' . $id); - } -} diff --git a/app/Console/Commands/ClearTasks.php b/app/Console/Commands/ClearTasks.php deleted file mode 100644 index 027f9915..00000000 --- a/app/Console/Commands/ClearTasks.php +++ /dev/null @@ -1,78 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Console\Commands; - -use Carbon; -use Pterodactyl\Models; -use Illuminate\Console\Command; -use Illuminate\Foundation\Bus\DispatchesJobs; - -class ClearTasks extends Command -{ - use DispatchesJobs; - - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'pterodactyl:tasks:clearlog'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Clears old log entires (> 2 months) from the last log.'; - - /** - * Create a new command instance. - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $entries = Models\TaskLog::where('run_time', '<=', Carbon::now()->subHours(config('pterodactyl.tasks.clear_log'))->toAtomString())->get(); - - $this->info(sprintf('Preparing to delete %d old task log entries.', count($entries))); - $bar = $this->output->createProgressBar(count($entries)); - - foreach ($entries as &$entry) { - $entry->delete(); - $bar->advance(); - } - - $bar->finish(); - $this->info("\nFinished deleting old logs."); - } -} diff --git a/app/Console/Commands/ClearServices.php b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php similarity index 51% rename from app/Console/Commands/ClearServices.php rename to app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php index c0438b36..36a96eef 100644 --- a/app/Console/Commands/ClearServices.php +++ b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php @@ -1,5 +1,5 @@ . * @@ -22,66 +22,61 @@ * SOFTWARE. */ -namespace Pterodactyl\Console\Commands; +namespace Pterodactyl\Console\Commands\Maintenance; -use DB; +use Carbon\Carbon; use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; -class ClearServices extends Command +class CleanServiceBackupFilesCommand extends Command { /** - * The name and signature of the console command. - * - * @var string + * @var \Carbon\Carbon */ - protected $signature = 'pterodactyl:clear-services'; + protected $carbon; /** - * The console command description. - * * @var string */ - protected $description = 'Removes all services from the database for installing updated ones as needed.'; + protected $description = 'Clean orphaned .bak files created when modifying services.'; /** - * Create a new command instance. + * @var \Illuminate\Contracts\Filesystem\Filesystem */ - public function __construct() + protected $disk; + + /** + * @var string + */ + protected $signature = 'p:maintenance:clean-service-backups'; + + /** + * CleanServiceBackupFilesCommand constructor. + * + * @param \Carbon\Carbon $carbon + * @param \Illuminate\Contracts\Filesystem\Factory $filesystem + */ + public function __construct(Carbon $carbon, FilesystemFactory $filesystem) { parent::__construct(); + + $this->carbon = $carbon; + $this->disk = $filesystem->disk(); } /** - * Execute the console command. - * - * @return mixed + * Handle command execution. */ public function handle() { - if (! $this->confirm('This is a destructive operation, are you sure you wish to continue?')) { - $this->error('Canceling.'); - exit(); - } + $files = $this->disk->files('services/.bak'); - $bar = $this->output->createProgressBar(3); - DB::beginTransaction(); - - try { - DB::table('services')->truncate(); - $bar->advance(); - - DB::table('service_options')->truncate(); - $bar->advance(); - - DB::table('service_variables')->truncate(); - $bar->advance(); - - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - } - - $this->info("\n"); - $this->info('All services have been removed. Consider running `php artisan pterodactyl:service-defaults` at this time.'); + collect($files)->each(function ($file) { + $lastModified = $this->carbon->timestamp($this->disk->lastModified($file)); + if ($lastModified->diffInMinutes($this->carbon->now()) > 5) { + $this->disk->delete($file); + $this->info(trans('command/messages.maintenance.deleting_service_backup', ['file' => $file])); + } + }); } } diff --git a/app/Console/Commands/RebuildServer.php b/app/Console/Commands/RebuildServer.php deleted file mode 100644 index c0e36535..00000000 --- a/app/Console/Commands/RebuildServer.php +++ /dev/null @@ -1,144 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Console\Commands; - -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Server; -use Illuminate\Console\Command; - -class RebuildServer extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'pterodactyl:rebuild - {--all} - {--node= : Id of node to rebuild all servers on.} - {--server= : UUID of server to rebuild.}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Rebuild docker containers for a server or multiple servers.'; - - /** - * Create a new command instance. - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - if ($this->option('all')) { - $servers = Server::all(); - } elseif ($this->option('node')) { - $servers = Server::where('node_id', $this->option('node'))->get(); - } elseif ($this->option('server')) { - $servers = Server::where('id', $this->option('server'))->get(); - } else { - $this->error('You must pass a flag to determine which server(s) to rebuild.'); - - return; - } - - $servers->load('node', 'service', 'option.variables', 'pack'); - - $this->line('Beginning processing, do not exit this script.'); - $bar = $this->output->createProgressBar(count($servers)); - $results = collect([]); - foreach ($servers as $server) { - try { - $environment = $server->option->variables->map(function ($item, $key) use ($server) { - $display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first(); - - return [ - 'variable' => $item->env_variable, - 'value' => (! is_null($display)) ? $display : $item->default_value, - ]; - }); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => [ - 'image' => $server->image, - 'env|overwrite' => $environment->pluck('value', 'variable')->merge(['STARTUP' => $server->startup]), - ], - 'service' => [ - 'type' => $server->service->folder, - 'option' => $server->option->tag, - 'pack' => ! is_null($server->pack) ? $server->pack->uuid : null, - ], - ], - ]); - - $results = $results->merge([ - $server->uuid => [ - 'status' => 'info', - 'messages' => [ - '[✓] Processed rebuild request for ' . $server->uuid, - ], - ], - ]); - } catch (\Exception $ex) { - $results = $results->merge([ - $server->uuid => [ - 'status' => 'error', - 'messages' => [ - '[✗] Failed to process rebuild request for ' . $server->uuid, - $ex->getMessage(), - ], - ], - ]); - } - - $bar->advance(); - } - - $bar->finish(); - $console = $this; - - $this->line("\n"); - $results->each(function ($item, $key) use ($console) { - foreach ($item['messages'] as $line) { - $console->{$item['status']}($line); - } - }); - $this->line("\nCompleted rebuild command processing."); - } -} diff --git a/app/Console/Commands/RunTasks.php b/app/Console/Commands/RunTasks.php deleted file mode 100644 index 9b1bff25..00000000 --- a/app/Console/Commands/RunTasks.php +++ /dev/null @@ -1,79 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Console\Commands; - -use Carbon; -use Pterodactyl\Models\Task; -use Illuminate\Console\Command; -use Pterodactyl\Jobs\SendScheduledTask; -use Illuminate\Foundation\Bus\DispatchesJobs; - -class RunTasks extends Command -{ - use DispatchesJobs; - - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'pterodactyl:tasks'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Find and run scheduled tasks.'; - - /** - * Create a new command instance. - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $tasks = Task::where('queued', false)->where('active', true)->where('next_run', '<=', Carbon::now()->toAtomString())->get(); - - $this->info(sprintf('Preparing to queue %d tasks.', count($tasks))); - $bar = $this->output->createProgressBar(count($tasks)); - - foreach ($tasks as &$task) { - $bar->advance(); - $this->dispatch((new SendScheduledTask($task))->onQueue(config('pterodactyl.queues.low'))); - } - - $bar->finish(); - $this->info("\nFinished queuing tasks for running."); - } -} diff --git a/app/Console/Commands/Schedule/ProcessRunnableCommand.php b/app/Console/Commands/Schedule/ProcessRunnableCommand.php index 12c90fa1..ab2f8db6 100644 --- a/app/Console/Commands/Schedule/ProcessRunnableCommand.php +++ b/app/Console/Commands/Schedule/ProcessRunnableCommand.php @@ -87,23 +87,22 @@ class ProcessRunnableCommand extends Command $schedules = $this->repository->getSchedulesToProcess($this->carbon->now()->toAtomString()); $bar = $this->output->createProgressBar(count($schedules)); - foreach ($schedules as $schedule) { - if (! $schedule->tasks instanceof Collection || count($schedule->tasks) < 1) { - $bar->advance(); + $schedules->each(function ($schedule) use ($bar) { + if ($schedule->tasks instanceof Collection && count($schedule->tasks) > 0) { + $this->processScheduleService->handle($schedule); - return; - } - - $this->processScheduleService->handle($schedule); - if ($this->input->isInteractive()) { - $this->line(trans('command/messages.schedule.output_line', [ - 'schedule' => $schedule->name, - 'hash' => $schedule->hashid, - ])); + if ($this->input->isInteractive()) { + $bar->clear(); + $this->line(trans('command/messages.schedule.output_line', [ + 'schedule' => $schedule->name, + 'hash' => $schedule->hashid, + ])); + } } $bar->advance(); - } + $bar->display(); + }); $this->line(''); } diff --git a/app/Console/Commands/Server/RebuildServerCommand.php b/app/Console/Commands/Server/RebuildServerCommand.php new file mode 100644 index 00000000..b76c286b --- /dev/null +++ b/app/Console/Commands/Server/RebuildServerCommand.php @@ -0,0 +1,140 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Console\Commands\Server; + +use Webmozart\Assert\Assert; +use Illuminate\Console\Command; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Services\Servers\EnvironmentService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class RebuildServerCommand extends Command +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var string + */ + protected $description = 'Rebuild a single server, all servers on a node, or all servers on the panel.'; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $environmentService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var string + */ + protected $signature = 'p:server:rebuild + {server? : The ID of the server to rebuild.} + {--node= : ID of the node to rebuild all servers on. Ignored if server is passed.}'; + + /** + * RebuildServerCommand constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct( + DaemonServerRepositoryInterface $daemonRepository, + EnvironmentService $environmentService, + ServerRepositoryInterface $repository + ) { + parent::__construct(); + + $this->daemonRepository = $daemonRepository; + $this->environmentService = $environmentService; + $this->repository = $repository; + } + + /** + * Handle command execution. + */ + public function handle() + { + $servers = $this->getServersToProcess(); + $bar = $this->output->createProgressBar(count($servers)); + $results = []; + + $servers->each(function ($server) use ($bar, &$results) { + $bar->clear(); + $json = [ + 'build' => [ + 'image' => $server->image, + 'env|overwrite' => $this->environmentService->process($server), + ], + 'service' => [ + 'type' => $server->option->service->folder, + 'option' => $server->option->tag, + 'pack' => object_get($server, 'pack.uuid'), + 'skip_scripts' => $server->skip_scripts, + ], + 'rebuild' => true, + ]; + + try { + $this->daemonRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($server->node->daemonSecret) + ->update($json); + } catch (RequestException $exception) { + $this->output->error(trans('command/messages.server.rebuild_failed', [ + 'name' => $server->name, + 'id' => $server->id, + 'node' => $server->node->name, + 'message' => $exception->getMessage(), + ])); + } + + $bar->advance(); + $bar->display(); + }); + + $this->line(''); + } + + /** + * Return the servers to be rebuilt. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + private function getServersToProcess() + { + Assert::nullOrIntegerish($this->argument('server'), 'Value passed in server argument must be null or an integer, received %s.'); + Assert::nullOrIntegerish($this->option('node'), 'Value passed in node option must be null or integer, received %s.'); + + return $this->repository->getDataForRebuild($this->argument('server'), $this->option('node')); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index db3eddde..ccb6830e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -7,10 +7,12 @@ use Pterodactyl\Console\Commands\InfoCommand; use Pterodactyl\Console\Commands\User\MakeUserCommand; use Pterodactyl\Console\Commands\User\DeleteUserCommand; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Pterodactyl\Console\Commands\Server\RebuildServerCommand; use Pterodactyl\Console\Commands\Location\MakeLocationCommand; use Pterodactyl\Console\Commands\User\DisableTwoFactorCommand; use Pterodactyl\Console\Commands\Location\DeleteLocationCommand; use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand; +use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; class Kernel extends ConsoleKernel { @@ -20,6 +22,7 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ + CleanServiceBackupFilesCommand::class, DeleteLocationCommand::class, DeleteUserCommand::class, DisableTwoFactorCommand::class, @@ -27,6 +30,7 @@ class Kernel extends ConsoleKernel MakeLocationCommand::class, MakeUserCommand::class, ProcessRunnableCommand::class, + RebuildServerCommand::class, ]; /** @@ -36,8 +40,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - // $schedule->command('pterodactyl:tasks')->everyMinute()->withoutOverlapping(); -// $schedule->command('pterodactyl:tasks:clearlog')->twiceDaily(3, 15); -// $schedule->command('pterodactyl:cleanservices')->twiceDaily(1, 13); + $schedule->command('p:process:runnable')->everyMinute()->withoutOverlapping(); + $schedule->command('p:maintenance:clean-service-backups')->daily(); } } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 9413b160..356fd354 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -31,11 +31,20 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Returns a listing of all servers that exist including relationships. * - * @param int $paginate + * @param int|null $paginate * @return mixed */ public function getAllServers($paginate); + /** + * Return a collection of servers with their associated data for rebuild operations. + * + * @param int|null $server + * @param int|null $node + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getDataForRebuild($server = null, $node = null); + /** * Return a server model and all variables associated with the server. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 227b7c22..31b5d96c 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -47,13 +47,30 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt */ public function getAllServers($paginate = 25) { - $instance = $this->getBuilder()->with('node', 'user', 'allocation'); + Assert::nullOrIntegerish($paginate, 'First argument passed to getAllServers must be integer or null, received %s.'); - if ($this->searchTerm) { - $instance->search($this->searchTerm); + $instance = $this->getBuilder()->with('node', 'user', 'allocation')->search($this->searchTerm); + + return is_null($paginate) ? $instance->get($this->getColumns()) : $instance->paginate($paginate, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getDataForRebuild($server = null, $node = null) + { + Assert::nullOrIntegerish($server, 'First argument passed to getDataForRebuild must be null or integer, received %s.'); + Assert::nullOrIntegerish($node, 'Second argument passed to getDataForRebuild must be null or integer, received %s.'); + + $instance = $this->getBuilder()->with('node', 'option.service', 'pack'); + + if (! is_null($server) && is_null($node)) { + $instance = $instance->where('id', '=', $server); + } elseif (is_null($server) && ! is_null($node)) { + $instance = $instance->where('node_id', '=', $node); } - return $instance->paginate($paginate); + return $instance->get($this->getColumns()); } /** @@ -62,6 +79,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt */ public function findWithVariables($id) { + Assert::integerish($id, 'First argument passed to findWithVariables must be integer, received %s.'); + $instance = $this->getBuilder()->with('option.variables', 'variables') ->where($this->getModel()->getKeyName(), '=', $id) ->first($this->getColumns()); diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index a8bd8517..f527c457 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -76,16 +76,12 @@ class EnvironmentService * * @param int|\Pterodactyl\Models\Server $server * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function process($server) { if (! $server instanceof Server) { - if (! is_numeric($server)) { - throw new \InvalidArgumentException( - 'First argument passed to process() must be an instance of \\Pterodactyl\\Models\\Server or numeric.' - ); - } - $server = $this->repository->find($server); } diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index c6540d6d..b4938bb9 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -54,4 +54,10 @@ return [ 'schedule' => [ 'output_line' => 'Dispatching job for first task in `:schedule` (:hash).', ], + 'maintenance' => [ + 'deleting_service_backup' => 'Deleting service backup file :file.', + ], + 'server' => [ + 'rebuild_failed' => 'Rebuild request for ":name" (#:id) on node ":node" failed with error: :message', + ], ]; diff --git a/app/Console/Commands/CleanServiceBackup.php b/tests/Unit/Commands/CommandTestCase.php similarity index 55% rename from app/Console/Commands/CleanServiceBackup.php rename to tests/Unit/Commands/CommandTestCase.php index bb56ab7f..5c26a06d 100644 --- a/app/Console/Commands/CleanServiceBackup.php +++ b/tests/Unit/Commands/CommandTestCase.php @@ -1,5 +1,5 @@ . * @@ -22,51 +22,34 @@ * SOFTWARE. */ -namespace Pterodactyl\Console\Commands; +namespace Tests\Unit\Commands; -use Carbon; -use Storage; +use Tests\TestCase; use Illuminate\Console\Command; +use Illuminate\Contracts\Foundation\Application; +use Symfony\Component\Console\Tester\CommandTester; -class CleanServiceBackup extends Command +abstract class CommandTestCase extends TestCase { /** - * The name and signature of the console command. + * Return the display from running a command. * - * @var string + * @param \Illuminate\Console\Command $command + * @param array $args + * @param array $inputs + * @param array $opts + * @return string */ - protected $signature = 'pterodactyl:cleanservices'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Cleans .bak files assocaited with service backups whene editing files through the panel.'; - - /** - * Create a new command instance. - */ - public function __construct() + protected function runCommand(Command $command, array $args = [], array $inputs = [], array $opts = []) { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $files = Storage::files('services/.bak'); - - foreach ($files as $file) { - $lastModified = Carbon::createFromTimestamp(Storage::lastModified($file)); - if ($lastModified->diffInMinutes(Carbon::now()) > 5) { - $this->info('Deleting ' . $file); - Storage::delete($file); - } + if (! $command->getLaravel() instanceof Application) { + $command->setLaravel($this->app); } + + $response = new CommandTester($command); + $response->setInputs($inputs); + $response->execute($args, $opts); + + return $response->getDisplay(); } } diff --git a/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php b/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php new file mode 100644 index 00000000..d6fd1802 --- /dev/null +++ b/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php @@ -0,0 +1,110 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Commands\Maintenance; + +use Mockery as m; +use Carbon\Carbon; +use Tests\TestCase; +use Illuminate\Contracts\Filesystem\Factory; +use Illuminate\Contracts\Filesystem\Filesystem; +use Symfony\Component\Console\Tester\CommandTester; +use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; + +class CleanServiceBackupFilesCommandTest extends TestCase +{ + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var \Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand + */ + protected $command; + + /** + * @var \Illuminate\Contracts\Filesystem\Filesystem + */ + protected $disk; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $filesystem; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->carbon = m::mock(Carbon::class); + $this->disk = m::mock(Filesystem::class); + $this->filesystem = m::mock(Factory::class); + $this->filesystem->shouldReceive('disk')->withNoArgs()->once()->andReturn($this->disk); + + $this->command = new CleanServiceBackupFilesCommand($this->carbon, $this->filesystem); + $this->command->setLaravel($this->app); + } + + /** + * Test that a file is deleted if it is > 5min old. + */ + public function testCommandCleansFilesMoreThan5MinutesOld() + { + $this->disk->shouldReceive('files')->with('services/.bak')->once()->andReturn(['testfile.txt']); + $this->disk->shouldReceive('lastModified')->with('testfile.txt')->once()->andReturn('disk:last:modified'); + $this->carbon->shouldReceive('timestamp')->with('disk:last:modified')->once()->andReturnSelf(); + $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnNull(); + $this->carbon->shouldReceive('diffInMinutes')->with(null)->once()->andReturn(10); + $this->disk->shouldReceive('delete')->with('testfile.txt')->once()->andReturnNull(); + + $response = new CommandTester($this->command); + $response->execute([]); + + $display = $response->getDisplay(); + $this->assertNotEmpty($display); + $this->assertContains(trans('command/messages.maintenance.deleting_service_backup', ['file' => 'testfile.txt']), $display); + } + + /** + * Test that a file isn't deleted if it is < 5min old. + */ + public function testCommandDoesNotCleanFileLessThan5MinutesOld() + { + $this->disk->shouldReceive('files')->with('services/.bak')->once()->andReturn(['testfile.txt']); + $this->disk->shouldReceive('lastModified')->with('testfile.txt')->once()->andReturn('disk:last:modified'); + $this->carbon->shouldReceive('timestamp')->with('disk:last:modified')->once()->andReturnSelf(); + $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnNull(); + $this->carbon->shouldReceive('diffInMinutes')->with(null)->once()->andReturn(2); + + $response = new CommandTester($this->command); + $response->execute([]); + + $display = $response->getDisplay(); + $this->assertEmpty($display); + } +} diff --git a/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php b/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php new file mode 100644 index 00000000..e474e98c --- /dev/null +++ b/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php @@ -0,0 +1,131 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Commands\Schedule; + +use Mockery as m; +use Carbon\Carbon; +use Pterodactyl\Models\Task; +use Pterodactyl\Models\Schedule; +use Tests\Unit\Commands\CommandTestCase; +use Pterodactyl\Services\Schedules\ProcessScheduleService; +use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand; +use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; + +class ProcessRunnableCommandTest extends CommandTestCase +{ + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var \Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand + */ + protected $command; + + /** + * @var \Pterodactyl\Services\Schedules\ProcessScheduleService + */ + protected $processScheduleService; + + /** + * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->carbon = m::mock(Carbon::class); + $this->processScheduleService = m::mock(ProcessScheduleService::class); + $this->repository = m::mock(ScheduleRepositoryInterface::class); + + $this->command = new ProcessRunnableCommand($this->carbon, $this->processScheduleService, $this->repository); + } + + /** + * Test that a schedule can be queued up correctly. + */ + public function testScheduleIsQueued() + { + $schedule = factory(Schedule::class)->make(); + $schedule->tasks = collect([factory(Task::class)->make()]); + + $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toAtomString')->withNoArgs()->once()->andReturn('00:00:00'); + $this->repository->shouldReceive('getSchedulesToProcess')->with('00:00:00')->once()->andReturn(collect([$schedule])); + $this->processScheduleService->shouldReceive('handle')->with($schedule)->once()->andReturnNull(); + + $display = $this->runCommand($this->command); + $this->assertNotEmpty($display); + $this->assertContains(trans('command/messages.schedule.output_line', [ + 'schedule' => $schedule->name, + 'hash' => $schedule->hashid, + ]), $display); + } + + /** + * If tasks is an empty collection, don't process it. + */ + public function testScheduleWithNoTasksIsNotProcessed() + { + $schedule = factory(Schedule::class)->make(); + $schedule->tasks = collect([]); + + $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toAtomString')->withNoArgs()->once()->andReturn('00:00:00'); + $this->repository->shouldReceive('getSchedulesToProcess')->with('00:00:00')->once()->andReturn(collect([$schedule])); + + $display = $this->runCommand($this->command); + $this->assertNotEmpty($display); + $this->assertNotContains(trans('command/messages.schedule.output_line', [ + 'schedule' => $schedule->name, + 'hash' => $schedule->hashid, + ]), $display); + } + + /** + * If tasks isn't an instance of a collection, don't process it. + */ + public function testScheduleWithTasksObjectThatIsNotInstanceOfCollectionIsNotProcessed() + { + $schedule = factory(Schedule::class)->make(['tasks' => null]); + + $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toAtomString')->withNoArgs()->once()->andReturn('00:00:00'); + $this->repository->shouldReceive('getSchedulesToProcess')->with('00:00:00')->once()->andReturn(collect([$schedule])); + + $display = $this->runCommand($this->command); + $this->assertNotEmpty($display); + $this->assertNotContains(trans('command/messages.schedule.output_line', [ + 'schedule' => $schedule->name, + 'hash' => $schedule->hashid, + ]), $display); + } +}