Add support for allocation management on nodes.

Allows deleting entire IP blocks, as well as allocating new IPs and
Ports via CIDR ranges, single IP, and single ports or a port range.
This commit is contained in:
Dane Everitt 2016-01-10 00:38:16 -05:00
parent aaf9768a92
commit 179481c547
8 changed files with 282 additions and 8 deletions

View file

@ -68,7 +68,7 @@ class NodesController extends Controller
{
$node = Models\Node::findOrFail($id);
$allocations = [];
$alloc = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $node->id)->get();
$alloc = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $node->id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get();
if ($alloc) {
foreach($alloc as &$alloc) {
if (!array_key_exists($alloc->ip, $allocations)) {
@ -122,9 +122,15 @@ class NodesController extends Controller
])->withInput();
}
public function deletePortAllocation(Request $request, $id, $ip, $port)
public function deleteAllocation(Request $request, $id, $ip, $port = null)
{
$allocation = Models\Allocation::where('node', $id)->whereNull('assigned_to')->where('ip', $ip)->where('port', $port)->first();
$query = Models\Allocation::where('node', $id)->whereNull('assigned_to')->where('ip', $ip);
if (is_null($port) || $port === 'undefined') {
$allocation = $query;
} else {
$allocation = $query->where('port', $port)->first();
}
if (!$allocation) {
return response()->json([
'error' => 'Unable to find an allocation matching those details to delete.'
@ -134,4 +140,50 @@ class NodesController extends Controller
return response('', 204);
}
public function getAllocationsJson(Request $request, $id)
{
$allocations = Models\Allocation::select('ip')->where('node', $id)->groupBy('ip')->get();
return response()->json($allocations);
}
public function postAllocations(Request $request, $id)
{
$processedData = [];
foreach($request->input('allocate_ip') as $ip) {
if (!array_key_exists($ip, $processedData)) {
$processedData[$ip] = [];
}
}
foreach($request->input('allocate_port') as $portid => $ports) {
if (array_key_exists($portid, $request->input('allocate_ip'))) {
$json = json_decode($ports);
if (json_last_error() === 0 && !empty($json)) {
foreach($json as &$parsed) {
array_push($processedData[$request->input('allocate_ip')[$portid]], $parsed->value);
}
}
}
}
try {
if(empty($processedData)) {
throw new \Pterodactyl\Exceptions\DisplayException('It seems that no data was passed to this function.');
}
$node = new NodeRepository;
$node->addAllocations($id, $processedData);
Alert::success('Successfully added new allocations to this node.')->flash();
} catch (\Pterodactyl\Exceptions\DisplayException $e) {
Alert::danger($e->getMessage())->flash();
} catch (\Exception $e) {
Log::error($e);
Alert::danger('An unhandled exception occured while attempting to add allocations this node. Please try again.')->flash();
} finally {
return redirect()->route('admin.nodes.view', [
'id' => $id,
'tab' => 'tab_allocation'
]);
}
}
}

View file

@ -172,8 +172,18 @@ class AdminRoutes {
'uses' => 'Admin\NodesController@postView'
]);
$router->delete('/view/{id}/allocation/{ip}/{port}', [
'uses' => 'Admin\NodesController@deletePortAllocation'
$router->delete('/view/{id}/allocation/{ip}/{port?}', [
'uses' => 'Admin\NodesController@deleteAllocation'
]);
$router->get('/view/{id}/allocations.json', [
'as' => 'admin.nodes.view.allocations',
'uses' => 'Admin\NodesController@getAllocationsJson'
]);
$router->post('/view/{id}/allocations', [
'as' => 'admin.nodes.post.allocations',
'uses' => 'Admin\NodesController@postAllocations'
]);
});

View file

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

View file

@ -2,11 +2,13 @@
namespace Pterodactyl\Repositories;
use DB;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
use IPTools\Network;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\DisplayValidationException;
@ -123,4 +125,62 @@ class NodeRepository {
}
public function addAllocations($id, array $allocations)
{
$node = Models\Node::findOrFail($id);
DB::beginTransaction();
foreach($allocations as $rawIP => $ports) {
$parsedIP = Network::parse($rawIP);
foreach($parsedIP as $ip) {
foreach($ports as $port) {
if (!is_int($port) && !preg_match('/^(\d{1,5})-(\d{1,5})$/', $port)) {
throw new DisplayException('The mapping for ' . $port . ' is invalid and cannot be processed.');
}
if (preg_match('/^(\d{1,5})-(\d{1,5})$/', $port, $matches)) {
foreach(range($matches[1], $matches[2]) as $assignPort) {
$alloc = Models\Allocation::firstOrNew([
'node' => $node->id,
'ip' => $ip,
'port' => $assignPort
]);
if (!$alloc->exists) {
$alloc->fill([
'node' => $node->id,
'ip' => $ip,
'port' => $assignPort,
'assigned_to' => null
]);
$alloc->save();
}
}
} else {
$alloc = Models\Allocation::firstOrNew([
'node' => $node->id,
'ip' => $ip,
'port' => $port
]);
if (!$alloc->exists) {
$alloc->fill([
'node' => $node->id,
'ip' => $ip,
'port' => $port,
'assigned_to' => null
]);
$alloc->save();
}
}
}
}
}
try {
DB::commit();
return true;
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
}
}

View file

@ -13,7 +13,8 @@
"kris/laravel-form-builder": "^1.6",
"pragmarx/google2fa": "^0.7.1",
"webpatser/laravel-uuid": "^2.0",
"prologue/alerts": "^0.4.0"
"prologue/alerts": "^0.4.0",
"s1lentium/iptools": "^1.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

View file

@ -80,3 +80,46 @@ form .text-muted {margin: 0 0 -5.5px}
.label{border-radius: .25em;padding: .2em .6em .3em;}
kbd{border-radius: .25em}
.modal-open .modal {padding-left: 0px !important;padding-right: 0px !important;overflow-y: scroll;}
/**
* Pillboxes
*/
.fuelux .pillbox {
border-radius: 0 !important;
}
li.btn.btn-default.pill {
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
color:#fff;
background-color:#008cba;
border-color:#0079a1;
}
li.btn.btn-default.pill:active,li.btn.btn-default.pill:focus,li.btn.btn-default.pill:hover {
background-color:#006687;
border-color:#004b63
}
.fuelux .pillbox>.pill-group .form-control {
height: 26px !important;
}
.fuelux .pillbox .pillbox-input-wrap {
margin: 0 !important;
}
.btn-allocate-delete {
height:34px;
width:34px;
padding:0;
}
@media (max-width:992px){
.btn-allocate-delete {
margin-top:22px;
}
}

View file

@ -1,3 +1,13 @@
function randomKey(length) {
var text = '';
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for( var i=0; i < length; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
$(document).ready(function () {
$.urlParam=function(name){var results=new RegExp("[\\?&]"+name+"=([^&#]*)").exec(decodeURIComponent(window.location.href));if(results==null){return null}else{return results[1]||0}};function getPageName(url){var index=url.lastIndexOf("/")+1;var filenameWithExtension=url.substr(index);var filename=filenameWithExtension.split(".")[0];return filename}
function centerModal(element) {

View file

@ -6,8 +6,10 @@
@section('scripts')
@parent
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fuelux/3.13.0/css/fuelux.min.css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/4.2.1/highcharts.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.7/socket.io.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fuelux/3.13.0/js/fuelux.min.js"></script>
<script src="{{ asset('js/bootstrap-notify.min.js') }}"></script>
<script>
$(document).ready(function () {
@ -285,7 +287,7 @@
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<table class="table table-striped table-bordered table-hover">
<table class="table table-striped table-bordered table-hover" style="margin-bottom:0;">
<thead>
<td>IP Address</td>
<td>Ports</td>
@ -294,7 +296,7 @@
<tbody>
@foreach($allocations as $ip => $ports)
<tr>
<td>{{ $ip }}</td>
<td><span style="cursor:pointer" data-action="delete" data-ip="{{ $ip }}" data-total="{{ count($ports) }}" class="is-ipblock"><i class="fa fa-fw fa-square-o"></i></span> {{ $ip }}</td>
<td>
@foreach($ports as $id => $allocation)
@if (($id % 2) === 0)
@ -322,6 +324,51 @@
</tbody>
</table>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<h4 style="margin-top:0;">Allocate Additional Ports</h4>
<form action="{{ route('admin.nodes.post.allocations', $node->id) }}" method="POST">
<div class="row" id="duplicate">
<div class="col-md-4 fuelux">
<label for="" class="control-label">IP Address</label>
<div class="input-group input-append dropdown combobox allocationComboBox" data-initialize="combobox">
<input type="text" name="allocate_ip[]" class="form-control pillbox_ip" style="border-right:0;">
<div class="input-group-btn">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
@foreach($allocations as $ip => $ports)
<li data-action="alloc_dropdown_val" data-value="{{ $ip }}"><a href="#">{{ $ip }}</a></li>
@endforeach
</ul>
</div>
</div>
</div>
<div class="form-group col-md-7 col-xs-10 fuelux">
<label for="" class="control-label">Ports</label>
<div class="pillbox allocationPillbox" data-initialize="pillbox">
<ul class="clearfix pill-group">
<li class="pillbox-input-wrap btn-group">
<input type="text" class="form-control dropdown-toggle pillbox-add-item" placeholder="add port">
</li>
</ul>
</div>
<input name="allocate_port[]" type="hidden" class="pillboxMain"/>
</div>
<div class="form-group col-md-1 col-xs-2" style="margin-left: -10px;">
<label for="" class="control-label">&nbsp;</label>
<button class="btn btn-danger btn-allocate-delete removeClone disabled"><i class="fa fa-close"></i></button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr />
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Add Ports" />
<button class="btn btn-success btn-sm cloneElement">Add More Rows</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane" id="tab_servers">
@ -384,6 +431,36 @@ $(document).ready(function () {
placement: 'auto'
});
$('.cloneElement').on('click', function (event) {
event.preventDefault();
var cloned = $('#duplicate').clone();
var rnd = randomKey(10);
cloned.find('.allocationPillbox').removeClass('allocationPillbox').addClass('allocationPillbox_' + rnd);
cloned.find('.pillboxMain').removeClass('pillboxMain').addClass('pillbox_' + rnd);
cloned.find('.removeClone').removeClass('disabled');
cloned.find('.pillbox_ip').removeClass('pillbox_ip').addClass('pillbox_ip_' + rnd);
cloned.insertAfter('#duplicate');
$('.allocationPillbox_' + rnd).pillbox();
$('.allocationPillbox_' + rnd).on('added.fu.pillbox edited.fu.pillbox removed.fu.pillbox', function pillboxChanged() {
$('.pillbox_' + rnd).val(JSON.stringify($('.allocationPillbox_' + rnd).pillbox('items')));
});
$('.removeClone').on('click', function (event) {
event.preventDefault();
var element = $(this);
element.parent().parent().slideUp(function () {
element.remove();
$('.pillbox_' + rnd).remove();
$('.pillbox_ip_' + rnd).remove();
});
});
})
$('.allocationPillbox').pillbox();
$('.allocationComboBox').combobox();
$('.allocationPillbox').on('added.fu.pillbox edited.fu.pillbox removed.fu.pillbox', function pillboxChanged() {
$('.pillboxMain').val(JSON.stringify($('.allocationPillbox').pillbox('items')));
});
var notifySocketError = false;
var Status = {
0: 'Off',
@ -664,6 +741,20 @@ $(document).ready(function () {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).done(function (data) {
if (element.hasClass('is-ipblock')) {
var tMatched = 0;
element.parent().parent().find('*').each(function () {
if ($(this).attr('data-port') && $(this).attr('data-ip')) {
$(this).fadeOut();
tMatched++;
}
});
if (tMatched === element.data('total')) {
element.fadeOut();
$('li[data-action="alloc_dropdown_val"][data-value="' + deleteIp + '"]').remove();
element.parent().parent().slideUp().remove();
}
}
swal({
type: 'success',
title: 'Port Deleted!',