diff --git a/app/Console/Commands/RunTasks.php b/app/Console/Commands/RunTasks.php new file mode 100644 index 000000000..44b7be9a6 --- /dev/null +++ b/app/Console/Commands/RunTasks.php @@ -0,0 +1,83 @@ + + * + * 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 DB; +use Carbon; +use Pterodactyl\Models; +use Illuminate\Console\Command; +use Illuminate\Foundation\Bus\DispatchesJobs; + +use Pterodactyl\Jobs\SendScheduledTask; + +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. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $tasks = Models\Task::where('queued', 0)->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(Models\Server::findOrFail($task->server), $task)); + } + + $bar->finish(); + $this->info("\nFinished queuing tasks for running."); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 53c4cfbe9..ae0634028 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -17,6 +17,7 @@ class Kernel extends ConsoleKernel \Pterodactyl\Console\Commands\MakeUser::class, \Pterodactyl\Console\Commands\ShowVersion::class, \Pterodactyl\Console\Commands\UpdateEnvironment::class, + \Pterodactyl\Console\Commands\RunTasks::class, ]; /** @@ -27,7 +28,6 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->command('inspire') - ->hourly(); + $schedule->command('pterodactyl:tasks')->everyFiveMinutes()->withoutOverlapping(); } } diff --git a/app/Jobs/SendScheduledTask.php b/app/Jobs/SendScheduledTask.php new file mode 100644 index 000000000..ac1dece3f --- /dev/null +++ b/app/Jobs/SendScheduledTask.php @@ -0,0 +1,72 @@ +server = $server; + $this->task = $task; + + $task->queued = 1; + $task->save(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $time = Carbon::now(); + try { + if ($this->task->action === 'command') { + $repo = new CommandRepository($this->server); + $response = $repo->send($this->task->data); + } + + $this->task->fill([ + 'last_run' => $time, + 'next_run' => $time->addMonths($this->task->month)->addWeeks($this->task->week)->addDays($this->task->day)->addHours($this->task->hour)->addMinutes($this->task->minute)->addSeconds($this->task->second), + 'queued' => 0 + ]); + $this->task->save(); + } catch (\Exception $ex) { + $wasError = true; + $response = $ex->getMessage(); + throw $ex; + } finally { + $log = new Models\TaskLog; + $log->fill([ + 'task_id' => $this->task->id, + 'run_time' => $time, + 'run_status' => (int) isset($wasError), + 'response' => $response + ]); + $log->save(); + } + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 61688da70..7bfc37cec 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -109,7 +109,6 @@ class Node extends Model $nodeData = self::getByID($node); - // @TODO: Better solution to disabling verification. Security risk. self::$guzzle[$node] = new Client([ 'base_uri' => sprintf('%s://%s:%s/', $nodeData->scheme, $nodeData->fqdn, $nodeData->daemonListen), 'timeout' => 5.0, diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 000000000..cfac1f5b7 --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,63 @@ + + * + * 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\Models; + +use Illuminate\Database\Eloquent\Model; + +class Task extends Model +{ + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'tasks'; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id', 'created_at', 'updated_at']; + + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'id' => 'integer', + 'server' => 'integer', + 'queued' => 'integer', + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['last_run', 'next_run', 'created_at', 'updated_at']; + +} diff --git a/app/Models/TaskLog.php b/app/Models/TaskLog.php new file mode 100644 index 000000000..d2e2519d1 --- /dev/null +++ b/app/Models/TaskLog.php @@ -0,0 +1,63 @@ + + * + * 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\Models; + +use Illuminate\Database\Eloquent\Model; + +class TaskLog extends Model +{ + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'tasks_log'; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id', 'created_at', 'updated_at']; + + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'id' => 'integer', + 'task_id' => 'integer', + 'run_status' => 'integer' + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['run_time', 'created_at', 'updated_at']; + +} diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php new file mode 100644 index 000000000..f9b5bd5a0 --- /dev/null +++ b/app/Repositories/Daemon/CommandRepository.php @@ -0,0 +1,79 @@ + + * + * 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\Repositories\Daemon; + +use Pterodactyl\Models; +use Pterodactyl\Exceptions\DisplayException; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; + +class CommandRepository { + + protected $server; + protected $node; + protected $client; + + public function __construct($server) + { + $this->server = ($server instanceof Models\Server) ? $server : Models\Server::findOrFail($server); + $this->node = Models\Node::getByID($this->server->node); + $this->client = Models\Node::guzzleRequest($this->server->node); + } + + /** + * [send description] + * @param string $command + * @return boolean + * @throws DisplayException + * @throws RequestException + */ + public function send($command) + { + // We don't use the user's specific daemon secret here since we + // are assuming that a call to this function has been validated. + // Additionally not all calls to this will be from a logged in user. + // (e.g. task queue or API) + try { + $response = $this->client->request('POST', '/server/command', [ + 'headers' => [ + 'X-Access-Token' => $this->server->daemonSecret, + 'X-Access-Server' => $this->server->uuid + ], + 'json' => [ + 'command' => $command + ] + ]); + + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { + throw new DisplayException('Command sending responded with a non-200 error code.'); + } + + return true; + } catch (\Exception $ex) { + throw $ex; + } + } + +} diff --git a/composer.json b/composer.json index 2dbf143c2..da8a9b577 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "prologue/alerts": "^0.4.0", "s1lentium/iptools": "^1.0", "edvinaskrucas/settings": "^2.0", - "igaster/laravel-theme": "^1.1" + "igaster/laravel-theme": "^1.1", + "nesbot/carbon": "^1.21" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/config/app.php b/config/app.php index 2719bf26e..559b48fb8 100644 --- a/config/app.php +++ b/config/app.php @@ -180,6 +180,7 @@ return [ 'Blade' => Illuminate\Support\Facades\Blade::class, 'Bus' => Illuminate\Support\Facades\Bus::class, 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Carbon' => Carbon\Carbon::class, 'Config' => Illuminate\Support\Facades\Config::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class, diff --git a/database/migrations/2016_02_27_163411_add_tasks_table.php b/database/migrations/2016_02_27_163411_add_tasks_table.php new file mode 100644 index 000000000..c05fb1be4 --- /dev/null +++ b/database/migrations/2016_02_27_163411_add_tasks_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->integer('server')->unsigned(); + $table->string('action'); + $table->text('data'); + $table->tinyInteger('queued')->unsigned()->default(0); + $table->integer('month')->default(0); + $table->integer('week')->default(0); + $table->integer('day')->default(0); + $table->integer('hour')->default(0); + $table->integer('minute')->default(0); + $table->integer('second')->default(0); + $table->timestamp('last_run'); + $table->timestamp('next_run'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('tasks'); + } +} diff --git a/database/migrations/2016_02_27_163447_add_tasks_log_table.php b/database/migrations/2016_02_27_163447_add_tasks_log_table.php new file mode 100644 index 000000000..6d9352dff --- /dev/null +++ b/database/migrations/2016_02_27_163447_add_tasks_log_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('task_id')->unsigned(); + $table->timestamp('run_time'); + $table->integer('run_status')->unsigned(); + $table->text('response'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('tasks_log'); + } +}