From 6fd7c78f0c2e8e5cf5575ebf0c185c37ffdc980a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Oct 2016 20:05:29 -0400 Subject: [PATCH] Add server deletion to a queue. This action allows servers to be deleted, but only be soft-deleted for 10 minutes. After that time period the server will be completely removed from the database and daemon. This allows some safety if a server is accidentally deleted. Force deleting a server will still work. If the daemon is in-accessible the server will fail to be deleted. When server is soft-deleted admins can still view its information page in the admin CP, however the server will be suspended and inaccessible on the front-end or though the daemon. Admins can manually delete the server ahead of the delete timer, or if it failed to delete previously they can do an immediate retry. --- .env.example | 1 + app/Events/ServerDeleted.php | 44 ++++++++++++ .../Controllers/Admin/ServersController.php | 35 ++++++++-- app/Http/Routes/AdminRoutes.php | 5 ++ app/Jobs/DeleteServer.php | 67 ++++++++++++++++++ app/Jobs/SuspendServer.php | 65 +++++++++++++++++ app/Listeners/DeleteServerListener.php | 64 +++++++++++++++++ app/Models/Server.php | 22 +++--- app/Providers/EventServiceProvider.php | 4 +- app/Repositories/ServerRepository.php | 70 ++++++++++++++----- resources/views/admin/servers/index.blade.php | 17 ++++- resources/views/admin/servers/view.blade.php | 21 ++++-- 12 files changed, 377 insertions(+), 38 deletions(-) create mode 100644 app/Events/ServerDeleted.php create mode 100644 app/Jobs/DeleteServer.php create mode 100644 app/Jobs/SuspendServer.php create mode 100644 app/Listeners/DeleteServerListener.php diff --git a/.env.example b/.env.example index 3da515951..47166434c 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY=SomeRandomString3232RandomString APP_THEME=default APP_TIMEZONE=UTC APP_CLEAR_TASKLOG=720 +APP_DELETE_MINUTES=10 DB_HOST=localhost DB_PORT=3306 diff --git a/app/Events/ServerDeleted.php b/app/Events/ServerDeleted.php new file mode 100644 index 000000000..4451b01bc --- /dev/null +++ b/app/Events/ServerDeleted.php @@ -0,0 +1,44 @@ + + * + * 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\Events; + +use Illuminate\Queue\SerializesModels; + +class ServerDeleted +{ + use SerializesModels; + + public $server; + + /** + * Create a new event instance. + * + * @return void + */ + public function __construct($id) + { + $this->server = $id; + } + +} diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 05cbe134d..8f0b184c6 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -51,7 +51,7 @@ class ServersController extends Controller public function getIndex(Request $request) { - $query = Models\Server::select( + $query = Models\Server::withTrashed()->select( 'servers.*', 'nodes.name as a_nodeName', 'users.email as a_ownerEmail', @@ -84,7 +84,7 @@ class ServersController extends Controller $servers = $query->paginate(20); } catch (\Exception $ex) { Alert::warning('There was an error with the search parameters provided.'); - $servers = Models\Server::select( + $servers = Models\Server::withTrashed()->select( 'servers.*', 'nodes.name as a_nodeName', 'users.email as a_ownerEmail', @@ -112,7 +112,7 @@ class ServersController extends Controller public function getView(Request $request, $id) { - $server = Models\Server::select( + $server = Models\Server::withTrashed()->select( 'servers.*', 'nodes.name as a_nodeName', 'users.email as a_ownerEmail', @@ -394,7 +394,7 @@ class ServersController extends Controller try { $server = new ServerRepository; $server->deleteServer($id, $force); - Alert::success('Server was successfully deleted from the panel and the daemon.')->flash(); + Alert::success('Server has been marked for deletion on the system.')->flash(); return redirect()->route('admin.servers'); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); @@ -510,4 +510,31 @@ class ServersController extends Controller } } + public function postQueuedDeletionHandler(Request $request, $id) + { + try { + $repo = new ServerRepository; + if (!is_null($request->input('cancel'))) { + $repo->cancelDeletion($id); + Alert::success('Server deletion has been cancelled. This server will remain suspended until you unsuspend it.')->flash(); + return redirect()->route('admin.servers.view', $id); + } else if(!is_null($request->input('delete'))) { + $repo->deleteNow($id); + Alert::success('Server was successfully deleted from the system.')->flash(); + return redirect()->route('admin.servers'); + } else if(!is_null($request->input('force_delete'))) { + $repo->deleteNow($id, true); + Alert::success('Server was successfully force deleted from the system.')->flash(); + return redirect()->route('admin.servers'); + } + } catch (DisplayException $ex) { + Alert::danger($ex->getMessage())->flash(); + return redirect()->route('admin.servers.view', $id); + } catch (\Exception $ex) { + Log::error($ex); + Alert::danger('An unhandled error occured while attempting to perform this action.')->flash(); + return redirect()->route('admin.servers.view', $id); + } + } + } diff --git a/app/Http/Routes/AdminRoutes.php b/app/Http/Routes/AdminRoutes.php index f8df15274..bfc07b725 100644 --- a/app/Http/Routes/AdminRoutes.php +++ b/app/Http/Routes/AdminRoutes.php @@ -210,6 +210,11 @@ class AdminRoutes { 'uses' => 'Admin\ServersController@deleteServer' ]); + $router->post('/view/{id}/queuedDeletion', [ + 'uses' => 'Admin\ServersController@postQueuedDeletionHandler', + 'as' => 'admin.servers.post.queuedDeletion' + ]); + }); // Node Routes diff --git a/app/Jobs/DeleteServer.php b/app/Jobs/DeleteServer.php new file mode 100644 index 000000000..78e1e1f48 --- /dev/null +++ b/app/Jobs/DeleteServer.php @@ -0,0 +1,67 @@ + + * + * 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\Jobs; + +use DB; + +use Illuminate\Bus\Queueable; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Contracts\Queue\ShouldQueue; + +use Pterodactyl\Models; +use Pterodactyl\Repositories\ServerRepository; + +class DeleteServer extends Job implements ShouldQueue +{ + use InteractsWithQueue, SerializesModels; + + /** + * Id of server to be deleted. + * @var object + */ + protected $id; + + /** + * Create a new job instance. + * + * @param integer $server + * @return void + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $repo = new ServerRepository; + $repo->deleteNow($this->id); + } +} diff --git a/app/Jobs/SuspendServer.php b/app/Jobs/SuspendServer.php new file mode 100644 index 000000000..912f8a0a9 --- /dev/null +++ b/app/Jobs/SuspendServer.php @@ -0,0 +1,65 @@ + + * + * 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\Jobs; + +use Debugbar; +use Illuminate\Bus\Queueable; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Contracts\Queue\ShouldQueue; + +use Pterodactyl\Repositories\ServerRepository; + +class SuspendServer extends Job implements ShouldQueue +{ + use InteractsWithQueue, SerializesModels; + + /** + * ID of associated server model. + * @var object + */ + protected $id; + + /** + * Create a new job instance. + * + * @param integer $id + * @return void + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $repo = new ServerRepository; + $repo->suspend($this->id, true); + } +} diff --git a/app/Listeners/DeleteServerListener.php b/app/Listeners/DeleteServerListener.php new file mode 100644 index 000000000..6a7833504 --- /dev/null +++ b/app/Listeners/DeleteServerListener.php @@ -0,0 +1,64 @@ + + * + * 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\Listeners; + +use Carbon; + +use Pterodactyl\Events\ServerDeleted; +use Illuminate\Foundation\Bus\DispatchesJobs; + +use Pterodactyl\Jobs\SuspendServer; +use Pterodactyl\Jobs\DeleteServer; + +class DeleteServerListener +{ + + use DispatchesJobs; + + /** + * Create the event listener. + * + * @return void + */ + public function __construct() + { + // + } + + /** + * Handle the event. + * + * @param DeleteServerEvent $event + * @return void + */ + public function handle(ServerDeleted $event) + { + $this->dispatch((new SuspendServer($event->server))->onQueue(env('QUEUE_HIGH', 'high'))); + $this->dispatch( + (new DeleteServer($event->server)) + ->delay(Carbon::now()->addMinutes(env('APP_DELETE_MINUTES', 10))) + ->onQueue(env('QUEUE_STANDARD', 'standard')) + ); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index d9c2f958a..9948c7a10 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -26,12 +26,15 @@ namespace Pterodactyl\Models; use Auth; use Pterodactyl\Models\Subuser; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use Pterodactyl\Exceptions\DisplayException; class Server extends Model { + use SoftDeletes; + /** * The table associated with the model. * @@ -44,17 +47,21 @@ class Server extends Model * * @var array */ - protected $hidden = [ - 'daemonSecret', - 'sftp_password' - ]; + protected $hidden = ['daemonSecret', 'sftp_password']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['deleted_at']; /** * Fields that are not mass assignable. * * @var array */ - protected $guarded = ['id', 'installed', 'created_at', 'updated_at']; + protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; /** * Cast values to correct type. @@ -92,6 +99,7 @@ class Server extends Model */ public function __construct() { + parent::__construct(); self::$user = Auth::user(); } @@ -181,10 +189,6 @@ class Server extends Model $result = $query->first(); - if (!$result) { - throw new DisplayException('No server was found belonging to this user.'); - } - if(!is_null($result)) { $result->daemonSecret = self::getUserDaemonSecret($result); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d706f31ce..7ec0d48ae 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -13,8 +13,8 @@ class EventServiceProvider extends ServiceProvider * @var array */ protected $listen = [ - 'Pterodactyl\Events\SomeEvent' => [ - 'Pterodactyl\Listeners\EventListener', + 'Pterodactyl\Events\ServerDeleted' => [ + 'Pterodactyl\Listeners\DeleteServerListener', ], ]; diff --git a/app/Repositories/ServerRepository.php b/app/Repositories/ServerRepository.php index 979554800..47b1a4a97 100644 --- a/app/Repositories/ServerRepository.php +++ b/app/Repositories/ServerRepository.php @@ -33,6 +33,7 @@ use Pterodactyl\Models; use Pterodactyl\Services\UuidService; use Pterodactyl\Services\DeploymentService; use Pterodactyl\Notifications\ServerCreated; +use Pterodactyl\Events\ServerDeleted; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\AccountNotFoundException; @@ -767,11 +768,37 @@ class ServerRepository public function deleteServer($id, $force) { $server = Models\Server::findOrFail($id); - $node = Models\Node::findOrFail($server->node); DB::beginTransaction(); try { - // Delete Allocations + if ($force === 'force' || $force === true) { + $server->installed = 3; + $server->save(); + } + + $server->delete(); + DB::commit(); + + event(new ServerDeleted($server->id)); + } catch (\Exception $ex) { + DB::rollBack(); + throw $ex; + } + } + + public function deleteNow($id, $force = false) { + $server = Models\Server::withTrashed()->findOrFail($id); + $node = Models\Node::findOrFail($server->node); + + // Handle server being restored previously or + // an accidental queue. + if (!$server->trashed()) { + return; + } + + DB::beginTransaction(); + try { + // Unassign Allocations Models\Allocation::where('assigned_to', $server->id)->update([ 'assigned_to' => null ]); @@ -779,20 +806,23 @@ class ServerRepository // Remove Variables Models\ServerVariables::where('server_id', $server->id)->delete(); + // Remove Permissions (Foreign Key requires before Subusers) + Models\Permission::where('server_id', $server->id)->delete(); + // Remove SubUsers Models\Subuser::where('server_id', $server->id)->delete(); - // Remove Permissions - Models\Permission::where('server_id', $server->id)->delete(); - // Remove Downloads Models\Download::where('server', $server->uuid)->delete(); + // Clear Tasks + Models\Task::where('server', $server->id)->delete(); + // Delete Databases - $databases = Models\Database::select('id')->where('server_id', $server->id)->get(); + // This is the one un-recoverable point where + // transactions will not save us. $repository = new DatabaseRepository; - foreach($databases as &$database) { - // Use the repository to drop the database, we don't need to delete here because it is now gone. + foreach(Models\Database::select('id')->where('server_id', $server->id)->get() as &$database) { $repository->drop($database->id); } @@ -804,17 +834,16 @@ class ServerRepository ] ]); - $server->delete(); + $server->forceDelete(); DB::commit(); - return true; } catch (\GuzzleHttp\Exception\TransferException $ex) { - if ($force === 'force') { - $server->delete(); + // Set installed is set to 3 when force deleting. + if ($server->installed === 3 || $force) { + $server->forceDelete(); DB::commit(); - return true; } else { DB::rollBack(); - throw new DisplayException('An error occured while attempting to delete the server on the daemon.', $ex); + throw $ex; } } catch (\Exception $ex) { DB::rollBack(); @@ -822,6 +851,15 @@ class ServerRepository } } + public function cancelDeletion($id) + { + $server = Models\Server::withTrashed()->findOrFail($id); + $server->restore(); + + $server->installed = 1; + $server->save(); + } + public function toggleInstall($id) { $server = Models\Server::findOrFail($id); @@ -837,9 +875,9 @@ class ServerRepository * @param integer $id * @return boolean */ - public function suspend($id) + public function suspend($id, $deleted = false) { - $server = Models\Server::findOrFail($id); + $server = ($deleted) ? Models\Server::withTrashed()->findOrFail($id) : Models\Server::findOrFail($id); $node = Models\Node::findOrFail($server->node); DB::beginTransaction(); diff --git a/resources/views/admin/servers/index.blade.php b/resources/views/admin/servers/index.blade.php index d49d7a585..67a913037 100644 --- a/resources/views/admin/servers/index.blade.php +++ b/resources/views/admin/servers/index.blade.php @@ -49,8 +49,21 @@ @foreach ($servers as $server) - suspended === 1)class="warning"@endif data-server="{{ $server->uuidShort }}"> - {{ $server->name }}@if($server->suspended === 1) Suspended@endif + suspended === 1 && !$server->trashed()) + class="warning" + @elseif($server->trashed()) + class="danger" + @endif + data-server="{{ $server->uuidShort }}"> + + {{ $server->name }} + @if($server->suspended === 1 && !$server->trashed()) + Suspended + @elseif($server->trashed()) + Pending Deletion + @endif + {{ $server->a_ownerEmail }} {{ $server->a_nodeName }} {{ $server->username }} diff --git a/resources/views/admin/servers/view.blade.php b/resources/views/admin/servers/view.blade.php index db86bc28d..d6178d872 100644 --- a/resources/views/admin/servers/view.blade.php +++ b/resources/views/admin/servers/view.blade.php @@ -30,10 +30,21 @@
  • Servers
  • {{ $server->name }} ({{ $server->uuidShort}})
  • - @if($server->suspended === 1) -
    - This server is suspended and has no user access. Processes cannot be started and files cannot be modified. All API access is disabled unless using a master token. -
    + @if($server->suspended === 1 && !$server->trashed()) +
    + This server is suspended and has no user access. Processes cannot be started and files cannot be modified. All API access is disabled unless using a master token. +
    + @elseif($server->trashed()) +
    + This server is marked for deletion {{ Carbon::parse($server->deleted_at)->addMinutes(env('APP_DELETE_MINUTES', 10))->diffForHumans() }}. If you want to cancel this action simply click the button below. +

    +
    + + + + {!! csrf_field() !!} +
    +
    @endif @if($server->installed === 0)
    @@ -55,7 +66,7 @@ @if($server->installed !== 2)
  • Manage
  • @endif -
  • Delete
  • + @if(!$server->trashed())
  • Delete
  • @endif