Skip to content

Commit 179481c

Browse files
committed
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.
1 parent aaf9768 commit 179481c

File tree

8 files changed

+282
-8
lines changed

8 files changed

+282
-8
lines changed

app/Http/Controllers/Admin/NodesController.php

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function getView(Request $request, $id)
6868
{
6969
$node = Models\Node::findOrFail($id);
7070
$allocations = [];
71-
$alloc = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $node->id)->get();
71+
$alloc = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $node->id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get();
7272
if ($alloc) {
7373
foreach($alloc as &$alloc) {
7474
if (!array_key_exists($alloc->ip, $allocations)) {
@@ -122,9 +122,15 @@ public function postView(Request $request, $id)
122122
])->withInput();
123123
}
124124

125-
public function deletePortAllocation(Request $request, $id, $ip, $port)
125+
public function deleteAllocation(Request $request, $id, $ip, $port = null)
126126
{
127-
$allocation = Models\Allocation::where('node', $id)->whereNull('assigned_to')->where('ip', $ip)->where('port', $port)->first();
127+
$query = Models\Allocation::where('node', $id)->whereNull('assigned_to')->where('ip', $ip);
128+
if (is_null($port) || $port === 'undefined') {
129+
$allocation = $query;
130+
} else {
131+
$allocation = $query->where('port', $port)->first();
132+
}
133+
128134
if (!$allocation) {
129135
return response()->json([
130136
'error' => 'Unable to find an allocation matching those details to delete.'
@@ -134,4 +140,50 @@ public function deletePortAllocation(Request $request, $id, $ip, $port)
134140
return response('', 204);
135141
}
136142

143+
public function getAllocationsJson(Request $request, $id)
144+
{
145+
$allocations = Models\Allocation::select('ip')->where('node', $id)->groupBy('ip')->get();
146+
return response()->json($allocations);
147+
}
148+
149+
public function postAllocations(Request $request, $id)
150+
{
151+
$processedData = [];
152+
foreach($request->input('allocate_ip') as $ip) {
153+
if (!array_key_exists($ip, $processedData)) {
154+
$processedData[$ip] = [];
155+
}
156+
}
157+
158+
foreach($request->input('allocate_port') as $portid => $ports) {
159+
if (array_key_exists($portid, $request->input('allocate_ip'))) {
160+
$json = json_decode($ports);
161+
if (json_last_error() === 0 && !empty($json)) {
162+
foreach($json as &$parsed) {
163+
array_push($processedData[$request->input('allocate_ip')[$portid]], $parsed->value);
164+
}
165+
}
166+
}
167+
}
168+
169+
try {
170+
if(empty($processedData)) {
171+
throw new \Pterodactyl\Exceptions\DisplayException('It seems that no data was passed to this function.');
172+
}
173+
$node = new NodeRepository;
174+
$node->addAllocations($id, $processedData);
175+
Alert::success('Successfully added new allocations to this node.')->flash();
176+
} catch (\Pterodactyl\Exceptions\DisplayException $e) {
177+
Alert::danger($e->getMessage())->flash();
178+
} catch (\Exception $e) {
179+
Log::error($e);
180+
Alert::danger('An unhandled exception occured while attempting to add allocations this node. Please try again.')->flash();
181+
} finally {
182+
return redirect()->route('admin.nodes.view', [
183+
'id' => $id,
184+
'tab' => 'tab_allocation'
185+
]);
186+
}
187+
}
188+
137189
}

app/Http/Routes/AdminRoutes.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,18 @@ public function map(Router $router) {
172172
'uses' => 'Admin\NodesController@postView'
173173
]);
174174

175-
$router->delete('/view/{id}/allocation/{ip}/{port}', [
176-
'uses' => 'Admin\NodesController@deletePortAllocation'
175+
$router->delete('/view/{id}/allocation/{ip}/{port?}', [
176+
'uses' => 'Admin\NodesController@deleteAllocation'
177+
]);
178+
179+
$router->get('/view/{id}/allocations.json', [
180+
'as' => 'admin.nodes.view.allocations',
181+
'uses' => 'Admin\NodesController@getAllocationsJson'
182+
]);
183+
184+
$router->post('/view/{id}/allocations', [
185+
'as' => 'admin.nodes.post.allocations',
186+
'uses' => 'Admin\NodesController@postAllocations'
177187
]);
178188

179189
});

app/Models/Allocation.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ class Allocation extends Model
1414
*/
1515
protected $table = 'allocations';
1616

17+
/**
18+
* Fields that are not mass assignable.
19+
*
20+
* @var array
21+
*/
22+
protected $guarded = ['id', 'created_at', 'updated_at'];
23+
1724
}

app/Repositories/NodeRepository.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
namespace Pterodactyl\Repositories;
44

5+
use DB;
56
use Validator;
67

78
use Pterodactyl\Models;
89
use Pterodactyl\Services\UuidService;
910

11+
use IPTools\Network;
1012
use Pterodactyl\Exceptions\DisplayException;
1113
use Pterodactyl\Exceptions\DisplayValidationException;
1214

@@ -123,4 +125,62 @@ public function update($id, array $data)
123125

124126
}
125127

128+
public function addAllocations($id, array $allocations)
129+
{
130+
$node = Models\Node::findOrFail($id);
131+
132+
DB::beginTransaction();
133+
foreach($allocations as $rawIP => $ports) {
134+
$parsedIP = Network::parse($rawIP);
135+
foreach($parsedIP as $ip) {
136+
foreach($ports as $port) {
137+
if (!is_int($port) && !preg_match('/^(\d{1,5})-(\d{1,5})$/', $port)) {
138+
throw new DisplayException('The mapping for ' . $port . ' is invalid and cannot be processed.');
139+
}
140+
if (preg_match('/^(\d{1,5})-(\d{1,5})$/', $port, $matches)) {
141+
foreach(range($matches[1], $matches[2]) as $assignPort) {
142+
$alloc = Models\Allocation::firstOrNew([
143+
'node' => $node->id,
144+
'ip' => $ip,
145+
'port' => $assignPort
146+
]);
147+
if (!$alloc->exists) {
148+
$alloc->fill([
149+
'node' => $node->id,
150+
'ip' => $ip,
151+
'port' => $assignPort,
152+
'assigned_to' => null
153+
]);
154+
$alloc->save();
155+
}
156+
}
157+
} else {
158+
$alloc = Models\Allocation::firstOrNew([
159+
'node' => $node->id,
160+
'ip' => $ip,
161+
'port' => $port
162+
]);
163+
if (!$alloc->exists) {
164+
$alloc->fill([
165+
'node' => $node->id,
166+
'ip' => $ip,
167+
'port' => $port,
168+
'assigned_to' => null
169+
]);
170+
$alloc->save();
171+
}
172+
}
173+
}
174+
}
175+
}
176+
177+
try {
178+
DB::commit();
179+
return true;
180+
} catch (\Exception $ex) {
181+
DB::rollBack();
182+
throw $ex;
183+
}
184+
}
185+
126186
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"kris/laravel-form-builder": "^1.6",
1414
"pragmarx/google2fa": "^0.7.1",
1515
"webpatser/laravel-uuid": "^2.0",
16-
"prologue/alerts": "^0.4.0"
16+
"prologue/alerts": "^0.4.0",
17+
"s1lentium/iptools": "^1.0"
1718
},
1819
"require-dev": {
1920
"fzaninotto/faker": "~1.4",

public/css/pterodactyl.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,46 @@ form .text-muted {margin: 0 0 -5.5px}
8080
.label{border-radius: .25em;padding: .2em .6em .3em;}
8181
kbd{border-radius: .25em}
8282
.modal-open .modal {padding-left: 0px !important;padding-right: 0px !important;overflow-y: scroll;}
83+
84+
/**
85+
* Pillboxes
86+
*/
87+
.fuelux .pillbox {
88+
border-radius: 0 !important;
89+
}
90+
91+
li.btn.btn-default.pill {
92+
padding: 1px 5px;
93+
font-size: 12px;
94+
line-height: 1.5;
95+
border-radius: 3px;
96+
color:#fff;
97+
background-color:#008cba;
98+
border-color:#0079a1;
99+
}
100+
101+
li.btn.btn-default.pill:active,li.btn.btn-default.pill:focus,li.btn.btn-default.pill:hover {
102+
background-color:#006687;
103+
border-color:#004b63
104+
}
105+
106+
107+
.fuelux .pillbox>.pill-group .form-control {
108+
height: 26px !important;
109+
}
110+
111+
.fuelux .pillbox .pillbox-input-wrap {
112+
margin: 0 !important;
113+
}
114+
115+
.btn-allocate-delete {
116+
height:34px;
117+
width:34px;
118+
padding:0;
119+
}
120+
121+
@media (max-width:992px){
122+
.btn-allocate-delete {
123+
margin-top:22px;
124+
}
125+
}

public/js/admin.min.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
function randomKey(length) {
2+
var text = '';
3+
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
4+
5+
for( var i=0; i < length; i++ ) {
6+
text += possible.charAt(Math.floor(Math.random() * possible.length));
7+
}
8+
9+
return text;
10+
}
111
$(document).ready(function () {
212
$.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}
313
function centerModal(element) {

resources/views/admin/nodes/view.blade.php

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
@section('scripts')
88
@parent
9+
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fuelux/3.13.0/css/fuelux.min.css" />
910
<script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/4.2.1/highcharts.js"></script>
1011
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.7/socket.io.min.js"></script>
12+
<script src="//cdnjs.cloudflare.com/ajax/libs/fuelux/3.13.0/js/fuelux.min.js"></script>
1113
<script src="{{ asset('js/bootstrap-notify.min.js') }}"></script>
1214
<script>
1315
$(document).ready(function () {
@@ -285,7 +287,7 @@
285287
<div class="panel panel-default">
286288
<div class="panel-heading"></div>
287289
<div class="panel-body">
288-
<table class="table table-striped table-bordered table-hover">
290+
<table class="table table-striped table-bordered table-hover" style="margin-bottom:0;">
289291
<thead>
290292
<td>IP Address</td>
291293
<td>Ports</td>
@@ -294,7 +296,7 @@
294296
<tbody>
295297
@foreach($allocations as $ip => $ports)
296298
<tr>
297-
<td>{{ $ip }}</td>
299+
<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>
298300
<td>
299301
@foreach($ports as $id => $allocation)
300302
@if (($id % 2) === 0)
@@ -322,6 +324,51 @@
322324
</tbody>
323325
</table>
324326
</div>
327+
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
328+
<div class="panel-body">
329+
<h4 style="margin-top:0;">Allocate Additional Ports</h4>
330+
<form action="{{ route('admin.nodes.post.allocations', $node->id) }}" method="POST">
331+
<div class="row" id="duplicate">
332+
<div class="col-md-4 fuelux">
333+
<label for="" class="control-label">IP Address</label>
334+
<div class="input-group input-append dropdown combobox allocationComboBox" data-initialize="combobox">
335+
<input type="text" name="allocate_ip[]" class="form-control pillbox_ip" style="border-right:0;">
336+
<div class="input-group-btn">
337+
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
338+
<ul class="dropdown-menu dropdown-menu-right">
339+
@foreach($allocations as $ip => $ports)
340+
<li data-action="alloc_dropdown_val" data-value="{{ $ip }}"><a href="#">{{ $ip }}</a></li>
341+
@endforeach
342+
</ul>
343+
</div>
344+
</div>
345+
</div>
346+
<div class="form-group col-md-7 col-xs-10 fuelux">
347+
<label for="" class="control-label">Ports</label>
348+
<div class="pillbox allocationPillbox" data-initialize="pillbox">
349+
<ul class="clearfix pill-group">
350+
<li class="pillbox-input-wrap btn-group">
351+
<input type="text" class="form-control dropdown-toggle pillbox-add-item" placeholder="add port">
352+
</li>
353+
</ul>
354+
</div>
355+
<input name="allocate_port[]" type="hidden" class="pillboxMain"/>
356+
</div>
357+
<div class="form-group col-md-1 col-xs-2" style="margin-left: -10px;">
358+
<label for="" class="control-label">&nbsp;</label>
359+
<button class="btn btn-danger btn-allocate-delete removeClone disabled"><i class="fa fa-close"></i></button>
360+
</div>
361+
</div>
362+
<div class="row">
363+
<div class="col-md-12">
364+
<hr />
365+
{!! csrf_field() !!}
366+
<input type="submit" class="btn btn-sm btn-primary" value="Add Ports" />
367+
<button class="btn btn-success btn-sm cloneElement">Add More Rows</button>
368+
</div>
369+
</div>
370+
</form>
371+
</div>
325372
</div>
326373
</div>
327374
<div class="tab-pane" id="tab_servers">
@@ -384,6 +431,36 @@
384431
placement: 'auto'
385432
});
386433
434+
$('.cloneElement').on('click', function (event) {
435+
event.preventDefault();
436+
var cloned = $('#duplicate').clone();
437+
var rnd = randomKey(10);
438+
cloned.find('.allocationPillbox').removeClass('allocationPillbox').addClass('allocationPillbox_' + rnd);
439+
cloned.find('.pillboxMain').removeClass('pillboxMain').addClass('pillbox_' + rnd);
440+
cloned.find('.removeClone').removeClass('disabled');
441+
cloned.find('.pillbox_ip').removeClass('pillbox_ip').addClass('pillbox_ip_' + rnd);
442+
cloned.insertAfter('#duplicate');
443+
$('.allocationPillbox_' + rnd).pillbox();
444+
$('.allocationPillbox_' + rnd).on('added.fu.pillbox edited.fu.pillbox removed.fu.pillbox', function pillboxChanged() {
445+
$('.pillbox_' + rnd).val(JSON.stringify($('.allocationPillbox_' + rnd).pillbox('items')));
446+
});
447+
$('.removeClone').on('click', function (event) {
448+
event.preventDefault();
449+
var element = $(this);
450+
element.parent().parent().slideUp(function () {
451+
element.remove();
452+
$('.pillbox_' + rnd).remove();
453+
$('.pillbox_ip_' + rnd).remove();
454+
});
455+
});
456+
})
457+
458+
$('.allocationPillbox').pillbox();
459+
$('.allocationComboBox').combobox();
460+
$('.allocationPillbox').on('added.fu.pillbox edited.fu.pillbox removed.fu.pillbox', function pillboxChanged() {
461+
$('.pillboxMain').val(JSON.stringify($('.allocationPillbox').pillbox('items')));
462+
});
463+
387464
var notifySocketError = false;
388465
var Status = {
389466
0: 'Off',
@@ -664,6 +741,20 @@
664741
'X-CSRF-TOKEN': '{{ csrf_token() }}'
665742
}
666743
}).done(function (data) {
744+
if (element.hasClass('is-ipblock')) {
745+
var tMatched = 0;
746+
element.parent().parent().find('*').each(function () {
747+
if ($(this).attr('data-port') && $(this).attr('data-ip')) {
748+
$(this).fadeOut();
749+
tMatched++;
750+
}
751+
});
752+
if (tMatched === element.data('total')) {
753+
element.fadeOut();
754+
$('li[data-action="alloc_dropdown_val"][data-value="' + deleteIp + '"]').remove();
755+
element.parent().parent().slideUp().remove();
756+
}
757+
}
667758
swal({
668759
type: 'success',
669760
title: 'Port Deleted!',

0 commit comments

Comments
 (0)