Whats this? We can add new servers now?!

This commit is contained in:
Dane Everitt 2015-12-15 15:08:41 -05:00
parent 6289e7ae8d
commit 264431a271
10 changed files with 214 additions and 85 deletions

View file

@ -4,7 +4,9 @@ namespace Pterodactyl\Exceptions;
use Exception; use Exception;
use DisplayException; use DisplayException;
use Debugbar; use DisplayValidationException;
use AccountNotFoundException;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

View file

@ -2,15 +2,14 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Alert;
use Debugbar; use Debugbar;
use Pterodactyl\Models;
use Pterodactyl\Repositories\ServerRepository; use Pterodactyl\Repositories\ServerRepository;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Node; use Pterodactly\Exceptions\DisplayException;
use Pterodactyl\Models\Location; use Pterodactly\Exceptions\DisplayValidationException;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\Service;
use Pterodactyl\Models\ServiceOptions;
use Pterodactyl\Models\ServiceVariables;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -33,7 +32,7 @@ class ServersController extends Controller
public function getIndex(Request $request) public function getIndex(Request $request)
{ {
return view('admin.servers.index', [ return view('admin.servers.index', [
'servers' => Server::select('servers.*', 'nodes.name as a_nodeName', 'users.email as a_ownerEmail') 'servers' => Models\Server::select('servers.*', 'nodes.name as a_nodeName', 'users.email as a_ownerEmail')
->join('nodes', 'servers.node', '=', 'nodes.id') ->join('nodes', 'servers.node', '=', 'nodes.id')
->join('users', 'servers.owner', '=', 'users.id') ->join('users', 'servers.owner', '=', 'users.id')
->paginate(20), ->paginate(20),
@ -43,8 +42,8 @@ class ServersController extends Controller
public function getNew(Request $request) public function getNew(Request $request)
{ {
return view('admin.servers.new', [ return view('admin.servers.new', [
'locations' => Location::all(), 'locations' => Models\Location::all(),
'services' => Service::all() 'services' => Models\Service::all()
]); ]);
} }
@ -57,14 +56,26 @@ class ServersController extends Controller
{ {
try { try {
$server = new ServerRepository; $server = new ServerRepository;
$resp = $server->create($request->all()); $response = $server->create($request->all());
echo $resp . '<br />';
return redirect()->route('admin.servers.view', [ 'id' => $response ]);
} catch (\Exception $e) { } catch (\Exception $e) {
if ($e instanceof \Pterodactyl\Exceptions\DisplayValidationException) {
return redirect()->route('admin.servers.new')->withErrors(json_decode($e->getMessage()))->withInput();
} else if ($e instanceof \Pterodactyl\Exceptions\DisplayException) {
Alert::danger($e->getMessage())->flash();
} else {
Debugbar::addException($e); Debugbar::addException($e);
Alert::danger('An unhandled exception occured while attemping to add this server. Please try again.')->flash();
} }
return json_encode($request->all()); return redirect()->route('admin.servers.new')->withInput();
}
} }
@ -83,7 +94,7 @@ class ServersController extends Controller
], 500); ], 500);
} }
return response()->json(Node::select('id', 'name', 'public')->where('location', $request->input('location'))->get()); return response()->json(Models\Node::select('id', 'name', 'public')->where('location', $request->input('location'))->get());
} }
@ -102,7 +113,7 @@ class ServersController extends Controller
], 500); ], 500);
} }
$ips = Allocation::where('node', $request->input('node'))->whereNull('assigned_to')->get(); $ips = Models\Allocation::where('node', $request->input('node'))->whereNull('assigned_to')->get();
$listing = []; $listing = [];
foreach($ips as &$ip) { foreach($ips as &$ip) {
@ -131,7 +142,7 @@ class ServersController extends Controller
], 500); ], 500);
} }
return response()->json(ServiceOptions::select('id', 'name', 'docker_image')->where('parent_service', $request->input('service'))->orderBy('name', 'asc')->get()); return response()->json(Models\ServiceOptions::select('id', 'name', 'docker_image')->where('parent_service', $request->input('service'))->orderBy('name', 'asc')->get());
} }
@ -150,7 +161,7 @@ class ServersController extends Controller
], 500); ], 500);
} }
return response()->json(ServiceVariables::where('option_id', $request->input('option'))->get()); return response()->json(Models\ServiceVariables::where('option_id', $request->input('option'))->get());
} }

View file

@ -24,6 +24,13 @@ class Server extends Model
*/ */
protected $hidden = ['daemonSecret']; protected $hidden = ['daemonSecret'];
/**
* Fields that are not mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'installed', 'created_at', 'updated_at'];
/** /**
* @var array * @var array
*/ */

View file

@ -14,4 +14,11 @@ class ServerVariables extends Model
*/ */
protected $table = 'server_variables'; protected $table = 'server_variables';
/**
* Fields that are not mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
} }

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Repositories; namespace Pterodactyl\Repositories;
use DB; use DB;
use Debugbar;
use Validator; use Validator;
use Pterodactyl\Models; use Pterodactyl\Models;
@ -64,7 +65,7 @@ class ServerRepository
// Run validator, throw catchable and displayable exception if it fails. // Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules. // Exception includes a JSON result of failed validation rules.
if ($validator->fails()) { if ($validator->fails()) {
throw new DisplayValidationException(json_encode($validator->errors()->all())); throw new DisplayValidationException($validator->errors());
} }
// Get the User ID; user exists since we passed the 'exists:users,email' part of the validation // Get the User ID; user exists since we passed the 'exists:users,email' part of the validation
@ -91,6 +92,7 @@ class ServerRepository
// Check those Variables // Check those Variables
$variables = Models\ServiceVariables::where('option_id', $data['option'])->get(); $variables = Models\ServiceVariables::where('option_id', $data['option'])->get();
$variableList = [];
if ($variables) { if ($variables) {
foreach($variables as $variable) { foreach($variables as $variable) {
@ -100,7 +102,11 @@ class ServerRepository
throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.'); throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.');
} }
$data['env_' . $variable->env_variable] = $variable->default_value; $variableList = array_merge($variableList, [[
'var_id' => $variable->id,
'var_val' => $variable->default_value
]]);
continue; continue;
} }
@ -109,13 +115,91 @@ class ServerRepository
throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').'); throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').');
} }
$variableList = array_merge($variableList, [[
'var_id' => $variable->id,
'var_val' => $data['env_' . $variable->env_variable]
]]);
continue; continue;
} }
} }
return (new UuidService)->generateShort(); // Check Overallocation
//return $this->generateSFTPUsername($data['name']); if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
// Check memory limits
if (is_numeric($node->memory_overallocate)) {
$newMemory = $totals->memory + $data['memory'];
$memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
if($newMemory > $memoryLimit) {
throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.');
}
}
// Check Disk Limits
if (is_numeric($node->disk_overallocate)) {
$newDisk = $totals->disk + $data['disk'];
$diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
if($newDisk > $diskLimit) {
throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.');
}
}
}
DB::beginTransaction();
$uuid = new UuidService;
// Add Server to the Database
$server = new Models\Server;
$server->fill([
'uuid' => $uuid->generate('servers', 'uuid'),
'uuidShort' => $uuid->generateShort(),
'node' => $data['node'],
'name' => $data['name'],
'active' => 1,
'owner' => $user->id,
'memory' => $data['memory'],
'disk' => $data['disk'],
'io' => $data['io'],
'cpu' => $data['cpu'],
'ip' => $data['ip'],
'port' => $data['port'],
'service' => $data['service'],
'option' => $data['option'],
'daemonSecret' => $uuid->generate('servers', 'daemonSecret'),
'username' => $this->generateSFTPUsername($data['name'])
]);
$server->save();
// Mark Allocation in Use
$allocation->assigned_to = $server->id;
$allocation->save();
// Add Variables
foreach($variableList as $item) {
Models\ServerVariables::create([
'server_id' => $server->id,
'variable_id' => $item['var_id'],
'variable_value' => $item['var_val']
]);
}
try {
// Add logic for communicating with Wings to make the server in here.
// We should add the server regardless of the Wings response, but
// handle the error and then allow the server to be re-deployed.
DB::commit();
return $server->id;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} }

View file

@ -29,7 +29,7 @@ class UserRepository
$user = new User; $user = new User;
$uuid = new UuidService; $uuid = new UuidService;
$user->uuid = $uuid->table('users')->generate(); $user->uuid = $uuid->generate('users', 'uuid');
$user->username = $username; $user->username = $username;
$user->email = $email; $user->email = $email;

View file

@ -8,16 +8,6 @@ use Uuid;
class UuidService class UuidService
{ {
/**
* @var string
*/
protected $table = 'users';
/**
* @var string
*/
protected $field = 'uuid';
/** /**
* Constructor * Constructor
*/ */
@ -26,45 +16,23 @@ class UuidService
// //
} }
/**
* Set the table that we need to be checking in the database.
*
* @param string $table
* @return void
*/
public function table($table)
{
$this->table = $table;
return $this;
}
/**
* Set the field in the given table that we want to check for a unique UUID.
*
* @param string $field
* @return void
*/
public function field($field)
{
$this->field = $field;
return $this;
}
/** /**
* Generate a unique UUID validating against specified table and column. * Generate a unique UUID validating against specified table and column.
* Defaults to `users.uuid` * Defaults to `users.uuid`
* *
* @param string $table
* @param string $field
* @param integer $type The type of UUID to generate. * @param integer $type The type of UUID to generate.
* @return string * @return string
*/ */
public function generate($type = 4) public function generate($table = 'users', $field = 'uuid', $type = 4)
{ {
$return = false; $return = false;
do { do {
$uuid = LaravelUUID::generate($type); $uuid = Uuid::generate($type);
if (!DB::table($this->table)->where($this->field, $uuid)->exists()) { if (!DB::table($table)->where($field, $uuid)->exists()) {
$return = $uuid; $return = $uuid;
} }
@ -81,13 +49,15 @@ class UuidService
* @param string $field * @param string $field
* @return string * @return string
*/ */
public function generateShort($table = 'servers', $field = 'uuidShort') public function generateShort($table = 'servers', $field = 'uuidShort', $attachedUuid = null)
{ {
$return = false; $return = false;
do { do {
$short = substr(Uuid::generate(4), 0, 8); $short = (is_null($attachedUuid)) ? substr(Uuid::generate(4), 0, 8) : substr($attachedUuid, 0, 8);
$attachedUuid = null;
if (!DB::table($table)->where($field, $short)->exists()) { if (!DB::table($table)->where($field, $short)->exists()) {
$return = $short; $return = $short;
} }

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemoveDaemonTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('daemon');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::create('daemon', function (Blueprint $table) {
$table->increments('id')->unsigned();
$table->mediumInteger('server')->unsigned();
$table->string('parameter');
$table->text('value');
$table->tinyInteger('editable')->unsigned()->default(0);
$table->tinyInteger('visible')->unsigned()->default(0);
$table->text('regex')->nullable();
$table->timestamps();
});
}
}

View file

@ -12,6 +12,8 @@ return [
| |
*/ */
'validation_error' => 'An error occured while validating the data you submitted:',
'failed' => 'These credentials do not match our records.', 'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'view_as_admin' => 'You are viewing this server listing as an admin. As such, all servers installed on the system are displayed. Any servers that you are set as the owner of are marked with a blue dot to the left of their name.', 'view_as_admin' => 'You are viewing this server listing as an admin. As such, all servers installed on the system are displayed. Any servers that you are set as the owner of are marked with a blue dot to the left of their name.',

View file

@ -11,11 +11,21 @@
<li><a href="/admin/servers">Servers</a></li> <li><a href="/admin/servers">Servers</a></li>
<li class="active">Create New Server</li> <li class="active">Create New Server</li>
</ul> </ul>
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>{{ trans('strings.whoops') }}!</strong> {{ trans('base.validation_error') }}<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@foreach (Alert::getMessages() as $type => $messages) @foreach (Alert::getMessages() as $type => $messages)
@foreach ($messages as $message) @foreach ($messages as $message)
<div class="alert alert-{{ $type }} alert-dismissable" role="alert"> <div class="alert alert-{{ $type }} alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ $message }} {!! $message !!}
</div> </div>
@endforeach @endforeach
@endforeach @endforeach
@ -26,14 +36,14 @@
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="name" class="control-label">Server Name</label> <label for="name" class="control-label">Server Name</label>
<div> <div>
<input type="text" autocomplete="off" name="name" class="form-control" /> <input type="text" autocomplete="off" name="name" class="form-control" value="{{ old('name') }}" />
<p class="text-muted"><small><em>Character limits: <code>a-zA-Z0-9_-</code> and <code>[Space]</code> (max 35 characters)</em></small></p> <p class="text-muted"><small><em>Character limits: <code>a-zA-Z0-9_-</code> and <code>[Space]</code> (max 35 characters)</em></small></p>
</div> </div>
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="owner" class="control-label">Owner Email</label> <label for="owner" class="control-label">Owner Email</label>
<div> <div>
<input type="text" autocomplete="off" name="owner" class="form-control" /> <input type="text" autocomplete="off" name="owner" class="form-control" value="{{ old('owner') }}" />
</div> </div>
</div> </div>
</div> </div>
@ -46,7 +56,7 @@
<label for="location" class="control-label">Server Location</label> <label for="location" class="control-label">Server Location</label>
<div> <div>
<select name="location" id="getLocation" class="form-control"> <select name="location" id="getLocation" class="form-control">
<option></option> <option disabled selected> -- Select a Location</option>
@foreach($locations as $location) @foreach($locations as $location)
<option value="{{ $location->id }}">{{ $location->long }} ({{ $location->short }})</option> <option value="{{ $location->id }}">{{ $location->long }} ({{ $location->short }})</option>
@endforeach @endforeach
@ -58,7 +68,7 @@
<label for="node" class="control-label">Server Node</label> <label for="node" class="control-label">Server Node</label>
<div> <div>
<select name="node" id="getNode" class="form-control"> <select name="node" id="getNode" class="form-control">
<option></option> <option disabled selected> -- Select a Node</option>
</select> </select>
<p class="text-muted"><small>The node which this server will be deployed to.</small></p> <p class="text-muted"><small>The node which this server will be deployed to.</small></p>
</div> </div>
@ -69,7 +79,7 @@
<label for="ip" class="control-label">Server IP</label> <label for="ip" class="control-label">Server IP</label>
<div> <div>
<select name="ip" id="getIP" class="form-control"> <select name="ip" id="getIP" class="form-control">
<option></option> <option disabled selected> -- Select an IP</option>
</select> </select>
<p class="text-muted"><small>Select the main IP that this server will be listening on. You can assign additional open IPs and ports below.</small></p> <p class="text-muted"><small>Select the main IP that this server will be listening on. You can assign additional open IPs and ports below.</small></p>
</div> </div>
@ -89,28 +99,28 @@
<div class="form-group col-md-3 col-xs-6"> <div class="form-group col-md-3 col-xs-6">
<label for="memory" class="control-label">Memory</label> <label for="memory" class="control-label">Memory</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="memory" class="form-control" /> <input type="text" name="memory" class="form-control" value="{{ old('memory') }}"/>
<span class="input-group-addon">MB</span> <span class="input-group-addon">MB</span>
</div> </div>
</div> </div>
<div class="form-group col-md-3 col-xs-6"> <div class="form-group col-md-3 col-xs-6">
<label for="disk" class="control-label">Disk Space</label> <label for="disk" class="control-label">Disk Space</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="disk" class="form-control" /> <input type="text" name="disk" class="form-control" value="{{ old('disk') }}"/>
<span class="input-group-addon">MB</span> <span class="input-group-addon">MB</span>
</div> </div>
</div> </div>
<div class="form-group col-md-3 col-xs-6"> <div class="form-group col-md-3 col-xs-6">
<label for="cpu" class="control-label">CPU Limit</label> <label for="cpu" class="control-label">CPU Limit</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="cpu" value="0" class="form-control" /> <input type="text" name="cpu" value="0" class="form-control" value="{{ old('cpu') }}"/>
<span class="input-group-addon">%</span> <span class="input-group-addon">%</span>
</div> </div>
</div> </div>
<div class="form-group col-md-3 col-xs-6"> <div class="form-group col-md-3 col-xs-6">
<label for="io" class="control-label">Block I/O</label> <label for="io" class="control-label">Block I/O</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="io" value="500" class="form-control" /> <input type="text" name="io" value="500" class="form-control" value="{{ old('io') }}"/>
<span class="input-group-addon">I/O</span> <span class="input-group-addon">I/O</span>
</div> </div>
</div> </div>
@ -130,7 +140,7 @@
<label for="service" class="control-label">Service Type</label> <label for="service" class="control-label">Service Type</label>
<div> <div>
<select name="service" id="getService" class="form-control"> <select name="service" id="getService" class="form-control">
<option></option> <option disabled selected> -- Select a Service</option>
@foreach($services as $service) @foreach($services as $service)
<option value="{{ $service->id }}">{{ $service->name }}</option> <option value="{{ $service->id }}">{{ $service->name }}</option>
@endforeach @endforeach
@ -142,7 +152,7 @@
<label for="option" class="control-label">Service Option</label> <label for="option" class="control-label">Service Option</label>
<div> <div>
<select name="option" id="getOption" class="form-control"> <select name="option" id="getOption" class="form-control">
<option></option> <option disabled selected> -- Select a Service Option</option>
</select> </select>
<p class="text-muted"><small>Select the type of service that this server will be running.</small></p> <p class="text-muted"><small>Select the type of service that this server will be running.</small></p>
</div> </div>
@ -157,9 +167,9 @@
<label for="use_custom_image" class="control-label">Use Custom Docker Image</label> <label for="use_custom_image" class="control-label">Use Custom Docker Image</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"> <span class="input-group-addon">
<input type="checkbox" name="use_custom_image" /> <input @if(old('name') === 'use_custom_image')checked="checked"@endif type="checkbox" name="use_custom_image"/>
</span> </span>
<input type="text" class="form-control" name="custom_image_name" disabled /> <input type="text" class="form-control" name="custom_image_name" value="{{ old('custom_image_name') }}" disabled />
</div> </div>
<p class="text-muted"><small>If you would like to use a custom docker image for this server please enter it here. Most users can ignore this option.</small></p> <p class="text-muted"><small>If you would like to use a custom docker image for this server please enter it here. Most users can ignore this option.</small></p>
</div> </div>
@ -210,8 +220,8 @@ $(document).ready(function () {
currentNode = null; currentNode = null;
// Hide Existing, and Reset contents // Hide Existing, and Reset contents
$('#getNode').html('<option></option>').parent().parent().addClass('hidden'); $('#getNode').html('<option disabled selected> -- Select a Node</option>').parent().parent().addClass('hidden');
$('#getIP').html('<option></option>').parent().parent().addClass('hidden'); $('#getIP').html('<option disabled selected> -- Select an IP</option>').parent().parent().addClass('hidden');
$('#getPort').html('').parent().parent().addClass('hidden'); $('#getPort').html('').parent().parent().addClass('hidden');
handleLoader('#load_settings', true); handleLoader('#load_settings', true);
@ -249,7 +259,7 @@ $(document).ready(function () {
currentNode = $('#getNode').val(); currentNode = $('#getNode').val();
// Hide Existing, and Reset contents // Hide Existing, and Reset contents
$('#getIP').html('<option></option>').parent().parent().addClass('hidden'); $('#getIP').html('<option disabled selected> -- Select an IP</option>').parent().parent().addClass('hidden');
$('#getPort').html('').parent().parent().addClass('hidden'); $('#getPort').html('').parent().parent().addClass('hidden');
handleLoader('#load_settings', true); handleLoader('#load_settings', true);