From 021710aa1c6b726dc4eba2549703800c1d169906 Mon Sep 17 00:00:00 2001
From: Dane Everitt <dane@daneeveritt.com>
Date: Fri, 2 Mar 2018 20:58:58 -0600
Subject: [PATCH] Add bulk power management via CLI

---
 CHANGELOG.md                                  |   1 +
 .../Server/BulkPowerActionCommand.php         | 119 ++++++++++++++++++
 .../Repository/ServerRepositoryInterface.php  |  19 +++
 .../Eloquent/ServerRepository.php             |  39 ++++++
 resources/lang/en/command/messages.php        |   4 +
 5 files changed, 182 insertions(+)
 create mode 100644 app/Console/Commands/Server/BulkPowerActionCommand.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d182ef4a..947098b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
 * Added proper transformer for Packs and re-enabled missing includes on server.
 * Added support for using Filesystem as a caching driver, although not recommended.
 * Added support for user management of server databases.
+* **Added bulk power management CLI interface to send start, stop, kill, restart actions to servers across configurable nodes.**
 
 ## v0.7.3 (Derelict Dermodactylus)
 ### Fixed
diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php
new file mode 100644
index 00000000..dbe36718
--- /dev/null
+++ b/app/Console/Commands/Server/BulkPowerActionCommand.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Pterodactyl\Console\Commands\Server;
+
+use Illuminate\Console\Command;
+use GuzzleHttp\Exception\RequestException;
+use Illuminate\Validation\Factory as ValidatorFactory;
+use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
+use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface;
+
+class BulkPowerActionCommand extends Command
+{
+    /**
+     * @var \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface
+     */
+    private $powerRepository;
+
+    /**
+     * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
+     */
+    private $repository;
+
+    /**
+     * @var \Illuminate\Validation\Factory
+     */
+    private $validator;
+
+    /**
+     * @var string
+     */
+    protected $signature = 'p:server:bulk-power
+                            {action : The action to perform (start, stop, restart, kill)}
+                            {--servers= : A comma seperated list of servers.}
+                            {--nodes= : A comma seperated list of nodes.}';
+
+    /**
+     * @var string
+     */
+    protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
+
+    /**
+     * BulkPowerActionCommand constructor.
+     *
+     * @param \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface $powerRepository
+     * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface       $repository
+     * @param \Illuminate\Validation\Factory                                    $validator
+     */
+    public function __construct(
+        PowerRepositoryInterface $powerRepository,
+        ServerRepositoryInterface $repository,
+        ValidatorFactory $validator
+    ) {
+        parent::__construct();
+
+        $this->powerRepository = $powerRepository;
+        $this->repository = $repository;
+        $this->validator = $validator;
+    }
+
+    /**
+     * Handle the bulk power request.
+     *
+     * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException
+     */
+    public function handle()
+    {
+        $action = $this->argument('action');
+        $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
+        $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
+
+        $validator = $this->validator->make([
+            'action' => $action,
+            'nodes' => $nodes,
+            'servers' => $servers,
+        ], [
+            'action' => 'string|in:start,stop,kill,restart',
+            'nodes' => 'array',
+            'nodes.*' => 'integer|min:1',
+            'servers' => 'array',
+            'servers.*' => 'integer|min:1',
+        ]);
+
+        if ($validator->fails()) {
+            foreach ($validator->getMessageBag()->all() as $message) {
+                $this->output->error($message);
+            }
+
+            return;
+        }
+
+        $count = $this->repository->getServersForPowerActionCount($servers, $nodes);
+        if (! $this->confirm(trans('command/messages.server.power.confirm', ['action' => $action, 'count' => $count]))) {
+            return;
+        }
+
+        $bar = $this->output->createProgressBar($count);
+        $servers = $this->repository->getServersForPowerAction($servers, $nodes);
+
+        foreach ($servers as $server) {
+            $bar->clear();
+
+            try {
+                $this->powerRepository->setServer($server)->sendSignal($action);
+            } catch (RequestException $exception) {
+                $this->output->error(trans('command/messages.server.power.action_failed', [
+                    'name' => $server->name,
+                    'id' => $server->id,
+                    'node' => $server->node->name,
+                    'message' => $exception->getMessage(),
+                ]));
+            }
+
+            $bar->advance();
+            $bar->display();
+        }
+
+        $this->line('');
+    }
+}
diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php
index 0ca74bf4..983cf7e6 100644
--- a/app/Contracts/Repository/ServerRepositoryInterface.php
+++ b/app/Contracts/Repository/ServerRepositoryInterface.php
@@ -117,4 +117,23 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
      * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
      */
     public function getByUuid(string $uuid): Server;
+
+    /**
+     * Return all of the servers that should have a power action performed aganist them.
+     *
+     * @param int[] $servers
+     * @param int[] $nodes
+     * @param bool  $returnCount
+     * @return int|\Generator
+     */
+    public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false);
+
+    /**
+     * Return the total number of servers that will be affected by the query.
+     *
+     * @param int[] $servers
+     * @param int[] $nodes
+     * @return int
+     */
+    public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int;
 }
diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php
index 7bca1269..5a53d33f 100644
--- a/app/Repositories/Eloquent/ServerRepository.php
+++ b/app/Repositories/Eloquent/ServerRepository.php
@@ -264,6 +264,45 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
         }
     }
 
+    /**
+     * Return all of the servers that should have a power action performed aganist them.
+     *
+     * @param int[] $servers
+     * @param int[] $nodes
+     * @param bool  $returnCount
+     * @return int|\Generator
+     */
+    public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false)
+    {
+        $instance = $this->getBuilder();
+
+        if (! empty($nodes) && ! empty($servers)) {
+            $instance->whereIn('id', $servers)->orWhereIn('node_id', $nodes);
+        } elseif (empty($nodes) && ! empty($servers)) {
+            $instance->whereIn('id', $servers);
+        } elseif (! empty($nodes) && empty($servers)) {
+            $instance->whereIn('node_id', $nodes);
+        }
+
+        if ($returnCount) {
+            return $instance->count();
+        }
+
+        return $instance->with('node')->cursor();
+    }
+
+    /**
+     * Return the total number of servers that will be affected by the query.
+     *
+     * @param int[] $servers
+     * @param int[] $nodes
+     * @return int
+     */
+    public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int
+    {
+        return $this->getServersForPowerAction($servers, $nodes, true);
+    }
+
     /**
      * Return an array of server IDs that a given user can access based
      * on owner and subuser permissions.
diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php
index 77f67c66..4a525032 100644
--- a/resources/lang/en/command/messages.php
+++ b/resources/lang/en/command/messages.php
@@ -37,6 +37,10 @@ return [
     ],
     'server' => [
         'rebuild_failed' => 'Rebuild request for ":name" (#:id) on node ":node" failed with error: :message',
+        'power' => [
+            'confirm' => 'You are about to perform a :action aganist :count servers. Do you wish to continue?',
+            'action_failed' => 'Power action request for ":name" (#:id) on node ":node" failed with error: :message',
+        ],
     ],
     'environment' => [
         'mail' => [