Add build configuration to server management.

Allows modification of certain settings, as well as assigning
additional IP addresses and ports.
This commit is contained in:
Dane Everitt 2016-01-02 23:21:22 -05:00
parent 2c054e7edc
commit 4953608aee
4 changed files with 352 additions and 17 deletions

View file

@ -50,20 +50,24 @@ class ServersController extends Controller
public function getView(Request $request, $id) public function getView(Request $request, $id)
{ {
$server = Models\Server::select(
'servers.*',
'nodes.name as a_nodeName',
'users.email as a_ownerEmail',
'locations.long as a_locationName',
'services.name as a_serviceName',
'service_options.name as a_servceOptionName'
)->join('nodes', 'servers.node', '=', 'nodes.id')
->join('users', 'servers.owner', '=', 'users.id')
->join('locations', 'nodes.location', '=', 'locations.id')
->join('services', 'servers.service', '=', 'services.id')
->join('service_options', 'servers.option', '=', 'service_options.id')
->first();
return view('admin.servers.view', [ return view('admin.servers.view', [
'server' => Models\Server::select( 'server' => $server,
'servers.*', 'assigned' => Models\Allocation::select('id', 'ip', 'port')->where('assigned_to', $id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get(),
'nodes.name as a_nodeName', 'unassigned' => Models\Allocation::select('id', 'ip', 'port')->where('node', $server->node)->whereNull('assigned_to')->orderBy('ip', 'asc')->orderBy('port', 'asc')->get()
'users.email as a_ownerEmail',
'locations.long as a_locationName',
'services.name as a_serviceName',
'service_options.name as a_servceOptionName'
)->join('nodes', 'servers.node', '=', 'nodes.id')
->join('users', 'servers.owner', '=', 'users.id')
->join('locations', 'nodes.location', '=', 'locations.id')
->join('services', 'servers.service', '=', 'services.id')
->join('service_options', 'servers.option', '=', 'service_options.id')
->first()
]); ]);
} }
@ -225,4 +229,63 @@ class ServersController extends Controller
} }
} }
public function postUpdateServerToggleBuild(Request $request, $id) {
$server = Models\Server::findOrFail($id);
$node = Models\Node::findOrFail($server->node);
$client = Models\Node::guzzleRequest($server->node);
try {
$res = $client->request('POST', '/server/rebuild', [
'headers' => [
'X-Access-Server' => $server->uuid,
'X-Access-Token' => $node->daemonSecret
]
]);
Alert::success('A rebuild has been queued successfully. It will run the next time this server is booted.')->flash();
} catch (\GuzzleHttp\Exception\TransferException $ex) {
Log::warning($ex);
Alert::danger('An error occured while attempting to toggle a rebuild: ' . $ex->getMessage())->flash();
}
return redirect()->route('admin.servers.view', [
'id' => $id,
'tab' => 'tab_manage'
]);
}
public function postUpdateServerUpdateBuild(Request $request, $id)
{
try {
$server = new ServerRepository;
$server->changeBuild($id, [
'default' => $request->input('default'),
'add_additional' => $request->input('add_additional'),
'remove_additional' => $request->input('remove_additional'),
'memory' => $request->input('memory'),
'swap' => $request->input('swap'),
'io' => $request->input('io'),
'cpu' => $request->input('cpu'),
]);
Alert::success('Server details were successfully updated.')->flash();
} catch (\Exception $e) {
if ($e instanceof \Pterodactyl\Exceptions\DisplayValidationException) {
return redirect()->route('admin.servers.view', [
'id' => $id,
'tab' => 'tab_build'
])->withErrors(json_decode($e->getMessage()))->withInput();
} else if ($e instanceof \Pterodactyl\Exceptions\DisplayException) {
Alert::danger($e->getMessage())->flash();
} else {
Log::error($e);
Alert::danger('An unhandled exception occured while attemping to add this server. Please try again.')->flash();
}
}
return redirect()->route('admin.servers.view', [
'id' => $id,
'tab' => 'tab_build'
]);
}
} }

View file

@ -31,6 +31,8 @@ class AdminRoutes {
$router->get('/view/{id}', [ 'as' => 'admin.servers.view', 'uses' => 'Admin\ServersController@getView' ]); $router->get('/view/{id}', [ 'as' => 'admin.servers.view', 'uses' => 'Admin\ServersController@getView' ]);
$router->post('/view/{id}/details', [ 'uses' => 'Admin\ServersController@postUpdateServerDetails' ]); $router->post('/view/{id}/details', [ 'uses' => 'Admin\ServersController@postUpdateServerDetails' ]);
$router->post('/view/{id}/rebuild', [ 'uses' => 'Admin\ServersController@postUpdateServerToggleBuild' ]);
$router->post('/view/{id}/build', [ 'uses' => 'Admin\ServersController@postUpdateServerUpdateBuild' ]);
$router->post('/new', [ 'uses' => 'Admin\ServersController@postNewServer']); $router->post('/new', [ 'uses' => 'Admin\ServersController@postNewServer']);
$router->post('/new/get-nodes', [ 'uses' => 'Admin\ServersController@postNewServerGetNodes' ]); $router->post('/new/get-nodes', [ 'uses' => 'Admin\ServersController@postNewServerGetNodes' ]);

View file

@ -226,7 +226,7 @@ class ServerRepository
// Validate Fields // Validate Fields
$validator = Validator::make($data, [ $validator = Validator::make($data, [
'owner' => 'email|exists:users,email', 'owner' => 'email|exists:users,email',
'name' => 'regex:([\w -]{4,35})' 'name' => 'regex:^([\w -]{4,35})$'
]); ]);
// Run validator, throw catchable and displayable exception if it fails. // Run validator, throw catchable and displayable exception if it fails.
@ -304,11 +304,169 @@ class ServerRepository
throw new DisplayException('Daemon returned a a non HTTP/204 error code. HTTP/' + $res->getStatusCode()); throw new DisplayException('Daemon returned a a non HTTP/204 error code. HTTP/' + $res->getStatusCode());
} }
} catch (\Exception $ex) { } catch (\Exception $ex) {
DB::rollback(); DB::rollBack();
Log::error($ex); Log::error($ex);
throw new DisplayException('An error occured while attempting to update this server\'s information.'); throw new DisplayException('An error occured while attempting to update this server\'s information.');
} }
} }
/**
* [changeBuild description]
* @param integer $id
* @param array $data
* @return boolean
*/
public function changeBuild($id, array $data)
{
$validator = Validator::make($data, [
'default' => [
'string',
'regex:/^(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5])):(\d{1,5})$/'
],
'add_additional' => 'array',
'remove_additional' => 'array',
'memory' => 'integer|min:0',
'swap' => 'integer|min:0',
'io' => 'integer|min:10|max:1000',
'cpu' => 'integer|min:0',
'disk' => 'integer|min:0'
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
DB::beginTransaction();
$server = Models\Server::findOrFail($id);
if (isset($data['default'])) {
list($ip, $port) = explode(':', $data['default']);
if ($ip === $server->ip && $port === $server->port) {
continue;
}
$allocation = Models\Allocation::where('ip', $ip)->where('port', $port)->where('assigned_to', $server->id)->get();
if (!$allocation) {
throw new DisplayException('The assigned default connection (' . $ip . ':' . $prot . ') is not allocated to this server.');
}
$server->ip = $ip;
$server->port = $port;
}
// Remove Assignments
if (isset($data['remove_additional'])) {
foreach ($data['remove_additional'] as $id => $combo) {
list($ip, $port) = explode(':', $combo);
// Invalid, not worth killing the whole thing, we'll just skip over it.
if (!filter_var($ip, FILTER_VALIDATE_IP) || !preg_match('/^(\d{1,5})$/', $port)) {
continue;
}
// Can't remove the assigned IP/Port combo
if ($ip === $server->ip && $port === $server->port) {
continue;
}
Models\Allocation::where('ip', $ip)->where('port', $port)->where('assigned_to', $server->id)->update([
'assigned_to' => null
]);
}
}
// Add Assignments
if (isset($data['add_additional'])) {
foreach ($data['add_additional'] as $id => $combo) {
list($ip, $port) = explode(':', $combo);
// Invalid, not worth killing the whole thing, we'll just skip over it.
if (!filter_var($ip, FILTER_VALIDATE_IP) || !preg_match('/^(\d{1,5})$/', $port)) {
continue;
}
// Don't allow double port assignments
if (Models\Allocation::where('port', $port)->where('assigned_to', $server->id)->count() !== 0) {
continue;
}
Models\Allocation::where('ip', $ip)->where('port', $port)->whereNull('assigned_to')->update([
'assigned_to' => $server->id
]);
}
}
// Loop All Assignments
$additionalAssignments = [];
$assignments = Models\Allocation::where('assigned_to', $server->id)->get();
foreach ($assignments as &$assignment) {
if (array_key_exists((string) $assignment->ip, $additionalAssignments)) {
array_push($additionalAssignments[ (string) $assignment->ip ], (int) $assignment->port);
} else {
$additionalAssignments[ (string) $assignment->ip ] = [ (int) $assignment->port ];
}
}
// @TODO: verify that server can be set to this much memory without
// going over node limits.
if (isset($data['memory'])) {
$server->memory = $data['memory'];
}
if (isset($data['swap'])) {
$server->swap = $data['swap'];
}
// @TODO: verify that server can be set to this much disk without
// going over node limits.
if (isset($data['disk'])) {
$server->disk = $data['disk'];
}
if (isset($data['cpu'])) {
$server->cpu = $data['cpu'];
}
if (isset($data['io'])) {
$server->io = $data['io'];
}
try {
$node = Models\Node::getByID($server->node);
$client = Models\Node::guzzleRequest($server->node);
$client->request('PATCH', '/server', [
'headers' => [
'X-Access-Server' => $server->uuid,
'X-Access-Token' => $node->daemonSecret
],
'json' => [
'build' => [
'default' => [
'ip' => $server->ip,
'port' => (int) $server->port
],
'ports|overwrite' => $additionalAssignments,
'memory' => (int) $server->memory,
'swap' => (int) $server->swap,
'io' => (int) $server->io,
'cpu' => (int) $server->cpu,
'disk' => (int) $server->disk
]
]
]);
$server->save();
DB::commit();
return true;
} catch (\GuzzleHttp\Exception\TransferException $ex) {
DB::rollBack();
throw new DisplayException('An error occured while attempting to update the configuration: ' . $ex->getMessage());
}
}
} }

View file

@ -33,6 +33,7 @@
<li class="active"><a href="#tab_about" data-toggle="tab">About</a></li> <li class="active"><a href="#tab_about" data-toggle="tab">About</a></li>
<li><a href="#tab_details" data-toggle="tab">Details</a></li> <li><a href="#tab_details" data-toggle="tab">Details</a></li>
<li><a href="#tab_build" data-toggle="tab">Build Configuration</a></li> <li><a href="#tab_build" data-toggle="tab">Build Configuration</a></li>
<li><a href="#tab_startup" data-toggle="tab">Startup Settings</a></li>
<li><a href="#tab_manage" data-toggle="tab">Manage</a></li> <li><a href="#tab_manage" data-toggle="tab">Manage</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
@ -139,7 +140,103 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"></div> <div class="panel-heading"></div>
<div class="panel-body"> <div class="panel-body">
Build <form action="/admin/servers/view/{{ $server->id }}/build" method="POST">
<div class="row">
<div class="col-md-6 form-group {{ $errors->has('memory') ? 'has-error' : '' }}">
<label for="memory" class="control-label">Allocated Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" value="{{ old('memory', $server->memory) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="col-md-6 form-group {{ $errors->has('swap') ? 'has-error' : '' }}">
<label for="swap" class="control-label">Allocated Swap</label>
<div class="input-group">
<input type="text" name="swap" class="form-control" value="{{ old('swap', $server->swap) }}"/>
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted"><small>Setting this to <code>0</code> will disable swap space on this server.</small></p>
</div>
</div>
<div class="row">
<div class="col-md-6 form-group {{ $errors->has('cpu') ? 'has-error' : '' }}">
<label for="cpu" class="control-label">CPU Limit</label>
<div class="input-group">
<input type="text" name="cpu" class="form-control" value="{{ old('cpu', $server->cpu) }}"/>
<span class="input-group-addon">%</span>
</div>
<p class="text-muted"><small>Each <em>physical</em> core on the system is considered to be <code>100%</code>. Setting this value to <code>0</code> will allow a server to use CPU time without restrictions.</small></p>
</div>
<div class="col-md-6 form-group {{ $errors->has('io') ? 'has-error' : '' }}">
<label for="io" class="control-label">Block IO Proportion</label>
<div>
<input type="text" name="io" class="form-control" value="{{ old('io', $server->io) }}"/>
</div>
<p class="text-muted"><small>Changing this value can have negative effects on all containers on the system. We strongly recommend leaving this value as <code>500</code>.</small></p>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
Additional IPs and Ports can be assigned to this server for use by plugins or other software. The game port is what will show up for the user to use to connect to thier server, and what their configuration files will be forced to use for binding.
</div>
</div>
<div class="col-md-6 form-group">
<label for="default" class="control-label">Game Port</label>
@foreach ($assigned as $assignment)
<div class="input-group" style="margin:5px auto;">
<span class="input-group-addon">
<input type="radio" @if($assignment->ip == $server->ip && $assignment->port == $server->port) checked="checked" @endif name="default" value="{{ $assignment->ip }}:{{ $assignment->port }}"/>
</span>
<input type="text" class="form-control" value="{{ $assignment->ip }}:{{ $assignment->port }}" readonly />
</div>
@endforeach
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-12 form-group">
<label for="add_additional" class="control-label">Assign Additional Ports</label>
<div>
<select name="add_additional[]" class="form-control" multiple>
@foreach ($unassigned as $assignment)
<option value="{{ $assignment->ip }}:{{ $assignment->port }}">{{ $assignment->ip }}:{{ $assignment->port }}</option>
@endforeach
</select>
</div>
<p class="text-muted"><small>Please note that due to software limitations you cannot assign identical ports on different IPs to the same server. For example, you <strong>cannot</strong> assign both <code>192.168.0.5:25565</code> and <code>192.168.10.5:25565</code> to the same server.</small></p>
</div>
</div>
<div class="row">
<div class="col-md-12 form-group">
<label for="remove_additional" class="control-label">Remove Additional Ports</label>
<div>
<select name="remove_additional[]" class="form-control" multiple>
@foreach ($assigned as $assignment)
<option value="{{ $assignment->ip }}:{{ $assignment->port }}" @if($assignment->ip == $server->ip && $assignment->port == $server->port) disabled @endif>{{ $assignment->ip }}:{{ $assignment->port }}</option>
@endforeach
</select>
</div>
<p class="text-muted"><small>Simply select which ports you would like to remove from the list above. If you want to assign a port on a different IP that is already in use you can select it above and delete it down here.</small></p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Update Build Configuration" />
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane" id="tab_startup">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
Startup
</div> </div>
</div> </div>
</div> </div>
@ -147,7 +244,18 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"></div> <div class="panel-heading"></div>
<div class="panel-body"> <div class="panel-body">
Manage <div class="col-md-12">
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/rebuild" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-sm btn-primary">Rebuild Server Container</button>
</form>
</div>
<div class="col-md-8">
<p>This will trigger a rebuild of the server container when it next starts up. This is useful if you modified the server configuration file manually, or something just didn't work out correctly. Please be aware: if you manually updated the server's configuration file, you will need to restart the daemon before doing this, or it will be overwritten.</p>
<div class="alert alert-info">A rebuild will automatically occur whenever you edit build configuration settings for the server.</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -156,6 +264,10 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/servers']").addClass('active'); $('#sidebar_links').find("a[href='/admin/servers']").addClass('active');
$('input[name="default"]').on('change', function (event) {
$('select[name="remove_additional"]').find('option:disabled').prop('disabled', false);
$('select[name="remove_additional"]').find('option[value="' + $(this).val() + '"]').prop('disabled', true).prop('selected', false);
});
}); });
</script> </script>
@endsection @endsection