From 7013d1098775e0315d14835772204abacd29422a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 8 Feb 2016 18:03:02 -0500 Subject: [PATCH] Add basic support for per-server databases Still missing ability to define database servers --- .../Controllers/Admin/ServersController.php | 46 +++++- app/Http/Routes/AdminRoutes.php | 10 ++ app/Models/Database.php | 62 ++++++++ app/Models/DatabaseServer.php | 61 ++++++++ app/Repositories/DatabaseRepository.php | 146 ++++++++++++++++++ ...2016_02_07_172148_add_databases_tables.php | 36 +++++ ...2_07_181319_add_database_servers_table.php | 37 +++++ resources/views/admin/servers/view.blade.php | 106 +++++++++++++ 8 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 app/Models/Database.php create mode 100644 app/Models/DatabaseServer.php create mode 100644 app/Repositories/DatabaseRepository.php create mode 100644 database/migrations/2016_02_07_172148_add_databases_tables.php create mode 100644 database/migrations/2016_02_07_181319_add_database_servers_table.php diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index a4e40febb..b4c2b507b 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -29,6 +29,7 @@ use Log; use Pterodactyl\Models; use Pterodactyl\Repositories\ServerRepository; +use Pterodactyl\Repositories\DatabaseRepository; use Pterodactly\Exceptions\DisplayException; use Pterodactly\Exceptions\DisplayValidationException; @@ -95,7 +96,12 @@ class ServersController extends Controller ->join('server_variables', 'server_variables.variable_id', '=', 'service_variables.id') ->where('service_variables.option_id', $server->option) ->where('server_variables.server_id', $server->id) - ->get() + ->get(), + 'databases' => Models\Database::select('databases.*', 'database_servers.host as a_host', 'database_servers.port as a_port') + ->where('server', $server->id) + ->join('database_servers', 'database_servers.id', '=', 'databases.db_server') + ->get(), + 'db_servers' => Models\DatabaseServer::all() ]); } @@ -375,4 +381,42 @@ class ServersController extends Controller } } + public function postDatabase(Request $request, $id) + { + try { + $repo = new DatabaseRepository; + $repo->create($id, $request->except([ + '_token' + ])); + Alert::success('Added new database to this server.')->flash(); + } catch (\Pterodactyl\Exceptions\DisplayValidationException $ex) { + return redirect()->route('admin.servers.view', [ + 'id' => $id, + 'tab' => 'tab_database' + ])->withInput()->withErrors(json_decode($ex->getMessage()))->withInput(); + } catch (\Exception $ex) { + Log::error($ex); + Alert::danger('An exception occured while attempting to add a new database for this server.')->flash(); + } + + return redirect()->route('admin.servers.view', [ + 'id' => $id, + 'tab' => 'tab_database' + ])->withInput(); + } + + public function deleteDatabase(Request $request, $id, $database) + { + try { + $repo = new DatabaseRepository; + $repo->drop($database); + return response('', 204); + } catch (\Exception $ex) { + Log::error($ex); + return response()->json([ + 'error' => 'An exception occured while attempting to delete this database.' + ], 500); + } + } + } diff --git a/app/Http/Routes/AdminRoutes.php b/app/Http/Routes/AdminRoutes.php index 120db974d..3164e5c14 100644 --- a/app/Http/Routes/AdminRoutes.php +++ b/app/Http/Routes/AdminRoutes.php @@ -152,6 +152,16 @@ class AdminRoutes { 'uses' => 'Admin\ServersController@getView' ]); + // Database Stuffs + $router->post('/view/{id}/database', [ + 'as' => 'admin.servers.database', + 'uses' => 'Admin\ServersController@postDatabase' + ]); + + $router->delete('/view/{id}/database/{database}', [ + 'uses' => 'Admin\ServersController@deleteDatabase' + ]); + // Change Server Details $router->post('/view/{id}/details', [ 'uses' => 'Admin\ServersController@postUpdateServerDetails' diff --git a/app/Models/Database.php b/app/Models/Database.php new file mode 100644 index 000000000..c682b296e --- /dev/null +++ b/app/Models/Database.php @@ -0,0 +1,62 @@ + + * + * 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 Database extends Model +{ + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'databases'; + + /** + * The attributes excluded from the model's JSON form. + * + * @var array + */ + protected $hidden = ['password']; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id', 'created_at', 'updated_at']; + + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'server' => 'integer', + 'db_server' => 'integer', + ]; + +} diff --git a/app/Models/DatabaseServer.php b/app/Models/DatabaseServer.php new file mode 100644 index 000000000..2a58e3168 --- /dev/null +++ b/app/Models/DatabaseServer.php @@ -0,0 +1,61 @@ + + * + * 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 DatabaseServer extends Model +{ + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'database_servers'; + + /** + * The attributes excluded from the model's JSON form. + * + * @var array + */ + protected $hidden = ['password']; + + /** + * 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', + ]; + +} diff --git a/app/Repositories/DatabaseRepository.php b/app/Repositories/DatabaseRepository.php new file mode 100644 index 000000000..e911a02a5 --- /dev/null +++ b/app/Repositories/DatabaseRepository.php @@ -0,0 +1,146 @@ + + * + * 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; + +use Crypt; +use DB; +use Validator; + +use Pterodactyl\Models; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\DisplayValidationException; + +use Illuminate\Database\Capsule\Manager as Capsule; + +class DatabaseRepository { + + /** + * Adds a new database to a given database server. + * @param int $server Id of the server to add a database for. + * @param array $options Array of options for creating that database. + * @return void + */ + public function create($server, $options) + { + $server = Models\Server::findOrFail($server); + $validator = Validator::make($options, [ + 'db_server' => 'required|exists:database_servers,id', + 'database' => 'required|regex:/^\w{1,100}$/', + 'remote' => 'required|regex:/^[0-9%.]{1,15}$/', + ]); + + if ($validator->fails()) { + throw new DisplayValidationException($validator->errors()); + } + + DB::beginTransaction(); + + $db = new Models\Database; + $db->fill([ + 'server' => $server->id, + 'db_server' => $options['db_server'], + 'database' => $server->uuidShort . '_' . $options['database'], + 'username' => $server->uuidShort . '_' . str_random(7), + 'remote' => $options['remote'], + 'password' => Crypt::encrypt(str_random(20)) + ]); + $db->save(); + + // Contact Remote + $dbr = Models\DatabaseServer::findOrFail($options['db_server']); + + try { + + $capsule = new Capsule; + $capsule->addConnection([ + 'driver' => 'mysql', + 'host' => $dbr->host, + 'port' => $dbr->port, + 'database' => 'mysql', + 'username' => $dbr->username, + 'password' => Crypt::decrypt($dbr->password), + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '' + ]); + + $capsule->setAsGlobal(); + + Capsule::statement('CREATE DATABASE ' . $db->database); + Capsule::statement('CREATE USER \'' . $db->username . '\'@\'' . $db->remote . '\' IDENTIFIED BY \'' . Crypt::decrypt($db->password) . '\''); + Capsule::statement('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON ' . $db->database . '.* TO \'' . $db->username . '\'@\'' . $db->remote . '\''); + Capsule::statement('FLUSH PRIVILEGES'); + + DB::commit(); + return true; + } catch (\Exception $ex) { + DB::rollback(); + throw $ex; + } + } + + /** + * Drops a database from the associated MySQL Server + * @param int $database The ID of the database to drop. + * @return boolean + */ + public function drop($database) + { + $db = Models\Database::findOrFail($database); + $dbr = Models\DatabaseServer::findOrFail($db->db_server); + + try { + + DB::beginTransaction(); + + $capsule = new Capsule; + $capsule->addConnection([ + 'driver' => 'mysql', + 'host' => $dbr->host, + 'port' => $dbr->port, + 'database' => 'mysql', + 'username' => $dbr->username, + 'password' => Crypt::decrypt($dbr->password), + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '' + ]); + + $capsule->setAsGlobal(); + + Capsule::statement('DROP USER \'' . $db->username . '\'@\'' . $db->remote . '\''); + Capsule::statement('DROP DATABASE ' . $db->database); + + $db->delete(); + + DB::commit(); + return true; + } catch (\Exception $ex) { + DB::rollback(); + throw $ex; + } + + } + +} diff --git a/database/migrations/2016_02_07_172148_add_databases_tables.php b/database/migrations/2016_02_07_172148_add_databases_tables.php new file mode 100644 index 000000000..9a36a690a --- /dev/null +++ b/database/migrations/2016_02_07_172148_add_databases_tables.php @@ -0,0 +1,36 @@ +increments('id'); + $table->integer('server')->unsigned(); + $table->integer('db_server')->unsigned(); + $table->string('database')->unique(); + $table->string('username')->unique(); + $table->string('remote')->default('%'); + $table->text('password'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('databases'); + } +} diff --git a/database/migrations/2016_02_07_181319_add_database_servers_table.php b/database/migrations/2016_02_07_181319_add_database_servers_table.php new file mode 100644 index 000000000..9d6da726e --- /dev/null +++ b/database/migrations/2016_02_07_181319_add_database_servers_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('name'); + $table->string('host'); + $table->integer('port')->unsigned(); + $table->string('username'); + $table->text('password'); + $table->integer('max_databases')->unsigned()->nullable(); + $table->integer('linked_node')->unsigned()->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('database_servers'); + } +} diff --git a/resources/views/admin/servers/view.blade.php b/resources/views/admin/servers/view.blade.php index 3284844ab..1e158e9bf 100644 --- a/resources/views/admin/servers/view.blade.php +++ b/resources/views/admin/servers/view.blade.php @@ -45,6 +45,7 @@
  • Details
  • Build Configuration
  • Startup
  • +
  • Database
  • @endif @if($server->installed !== 2)
  • Manage
  • @@ -303,6 +304,76 @@ +
    +
    +
    +
    +

    Add New Database

    +
    +
    +
    + +
    +
    {{ $server->uuidShort }}_
    + +
    +
    +
    + +
    + +
    +

    Which IP to allow connections from. Standard MySQL wildcard notation allowed (e.g. 192.168.%.%).

    +
    +
    +
    +
    + + +
    +
    + +
    + {!! csrf_field() !!} + +
    +
    +
    +
    +
    + @if(count($databases) > 0) +
    +
    + + + + + + + + + + + + @foreach($databases as $database) + + + + + + + + @endforeach + +
    DatabaseUser (Connections From)PasswordDB Server
    {{ $database->database }}{{ $database->username }} ({{ $database->remote }}){{ Crypt::decrypt($database->password) }}{{ $database->a_host }}:{{ $database->a_port }}
    +
    + @endif +
    +
    @endif @if($server->installed !== 2)
    @@ -424,6 +495,41 @@ $(document).ready(function () { event.target.submit(); }); }); + $('[data-action="delete_database"]').click(function (event) { + event.preventDefault(); + var self = $(this); + swal({ + title: '', + type: 'warning', + text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.', + showCancelButton: true, + confirmButtonText: 'Delete', + confirmButtonColor: '#d9534f', + closeOnConfirm: false + }, function () { + $.ajax({ + method: 'DELETE', + url: '{{ route('admin.servers.database', $server->id) }}/' + self.data('database'), + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } + }).done(function () { + self.parent().parent().slideUp(); + swal({ + title: '', + type: 'success', + text: 'Successfully deleted this database.' + }); + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.' + }); + }); + }); + }); }); @endsection