Implement node view, cleanup other files.

Still in progress, need to do a lot of controller cleanup first and add
node deletion as well.
This commit is contained in:
Dane Everitt 2017-03-03 17:30:39 -05:00
parent 6c7fff1de0
commit fd9f1a68eb
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
16 changed files with 1335 additions and 149 deletions

View file

@ -29,6 +29,7 @@ use Log;
use Alert;
use Carbon;
use Validator;
use Javascript;
use Pterodactyl\Models;
use Illuminate\Http\Request;
use Pterodactyl\Exceptions\DisplayException;
@ -55,9 +56,13 @@ class NodesController extends Controller
public function getIndex(Request $request)
{
return view('admin.nodes.index', [
'nodes' => Models\Node::with('location')->withCount('servers')->paginate(20),
]);
$nodes = Models\Node::with('location')->withCount('servers');
if (! is_null($request->input('query'))) {
$nodes->search($request->input('query'));
}
return view('admin.nodes.index', ['nodes' => $nodes->paginate(25)]);
}
public function getNew(Request $request)
@ -117,6 +122,105 @@ class NodesController extends Controller
]);
}
/**
* Shows the index overview page for a specific node.
*
* @param Request $request
* @param int $id The ID of the node to display information for.
*
* @return \Illuminate\View\View
*/
public function viewIndex(Request $request, $id)
{
$node = Models\Node::with('location')->withCount('servers')->findOrFail($id);
$stats = collect(
Models\Server::select(
DB::raw('SUM(memory) as memory, SUM(disk) as disk')
)->where('node_id', $node->id)->first()
)->mapWithKeys(function ($item, $key) use ($node) {
$percent = ($item / $node->{$key}) * 100;
return [$key => [
'value' => $item,
'percent' => $percent,
'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'),
]];
})->toArray();
return view('admin.nodes.view.index', ['node' => $node, 'stats' => $stats]);
}
/**
* Shows the settings page for a specific node.
*
* @param Request $request
* @param int $id The ID of the node to display information for.
*
* @return \Illuminate\View\View
*/
public function viewSettings(Request $request, $id)
{
return view('admin.nodes.view.settings', [
'node' => Models\Node::findOrFail($id),
'locations' => Models\Location::all(),
]);
}
/**
* Shows the configuration page for a specific node.
*
* @param Request $request
* @param int $id The ID of the node to display information for.
*
* @return \Illuminate\View\View
*/
public function viewConfiguration(Request $request, $id)
{
return view('admin.nodes.view.configuration', [
'node' => Models\Node::findOrFail($id),
]);
}
/**
* Shows the allocation page for a specific node.
*
* @param Request $request
* @param int $id The ID of the node to display information for.
*
* @return \Illuminate\View\View
*/
public function viewAllocation(Request $request, $id)
{
$node = Models\Node::findOrFail($id);
$node->setRelation('allocations', $node->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50));
Javascript::put([
'node' => collect($node)->only(['id']),
]);
return view('admin.nodes.view.allocation', ['node' => $node]);
}
/**
* Shows the server listing page for a specific node.
*
* @param Request $request
* @param int $id The ID of the node to display information for.
*
* @return \Illuminate\View\View
*/
public function viewServers(Request $request, $id)
{
$node = Models\Node::with('servers.user', 'servers.service', 'servers.allocations')->findOrFail($id);
Javascript::put([
'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']),
]);
return view('admin.nodes.view.servers', [
'node' => $node,
]);
}
public function postView(Request $request, $id)
{
try {
@ -149,10 +253,18 @@ class NodesController extends Controller
])->withInput();
}
public function deallocateSingle(Request $request, $node, $allocation)
/**
* Removes a single allocation from a node.
*
* @param Request $request
* @param integer $node
* @param integer $allocation [description]
* @return mixed
*/
public function allocationRemoveSingle(Request $request, $node, $allocation)
{
$query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete();
if ((int) $query === 0) {
if ($query < 1) {
return response()->json([
'error' => 'Unable to find an allocation matching those details to delete.',
], 400);
@ -161,33 +273,40 @@ class NodesController extends Controller
return response('', 204);
}
public function deallocateBlock(Request $request, $node)
/**
* Remove all allocations for a specific IP at once on a node.
*
* @param Request $request
* @param integer $node
* @return mixed
*/
public function allocationRemoveBlock(Request $request, $node)
{
$query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('ip', $request->input('ip'))->delete();
if ((int) $query === 0) {
if ($query < 1) {
Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash();
return redirect()->route('admin.nodes.view', [
'id' => $node,
'tab' => 'tab_allocations',
]);
} else {
Alert::success('Deleted all unallocated ports for <code>' . $request->input('ip') . '</code>.')->flash();
}
Alert::success('Deleted all unallocated ports for <code>' . $request->input('ip') . '</code>.')->flash();
return redirect()->route('admin.nodes.view', [
'id' => $node,
'tab' => 'tab_allocation',
]);
return redirect()->route('admin.nodes.view.allocation', $node);
}
public function setAlias(Request $request, $node)
/**
* Sets an alias for a specific allocation on a node.
*
* @param Request $request
* @param integer $node
* @return mixed
*/
public function allocationSetAlias(Request $request, $node)
{
if (! $request->input('allocation')) {
if (! $request->input('allocation_id')) {
return response('Missing required parameters.', 422);
}
try {
$update = Models\Allocation::findOrFail($request->input('allocation'));
$update = Models\Allocation::findOrFail($request->input('allocation_id'));
$update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$update->save();
@ -197,6 +316,32 @@ class NodesController extends Controller
}
}
/**
* Creates new allocations on a node.
*
* @param Request $request
* @param integer $node
* @return \Illuminate\Http\RedirectResponse
*/
public function createAllocation(Request $request, $node)
{
$repo = new NodeRepository;
try {
$repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports']));
Alert::success('Successfully added new allocations!')->flash();
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.nodes.view.allocation', $node)->withErrors(json_decode($ex->getMessage()))->withInput();
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.')->flash();
}
return redirect()->route('admin.nodes.view.allocation', $node);
}
public function getAllocationsJson(Request $request, $id)
{
$allocations = Models\Allocation::select('ip')->where('node_id', $id)->groupBy('ip')->get();
@ -204,55 +349,6 @@ class NodesController extends Controller
return response()->json($allocations);
}
public function postAllocations(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'allocate_ip.*' => 'required|string',
'allocate_port.*' => 'required',
]);
if ($validator->fails()) {
return redirect()->route('admin.nodes.view', [
'id' => $id,
'tab' => 'tab_allocation',
])->withErrors($validator->errors())->withInput();
}
$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 {
$node = new NodeRepository;
$node->addAllocations($id, $processedData);
Alert::success('Successfully added new allocations to this node.')->flash();
} catch (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',
]);
}
}
public function deleteNode(Request $request, $id)
{
try {

View file

@ -232,27 +232,53 @@ class AdminRoutes
'uses' => 'Admin\NodesController@postNew',
]);
// View Node
$router->get('/view/{id}', [
$router->get('/view/{id}/do/index', [
'as' => 'admin.nodes.view',
'uses' => 'Admin\NodesController@getView',
'uses' => 'Admin\NodesController@viewIndex',
]);
$router->post('/view/{id}', [
'uses' => 'Admin\NodesController@postView',
$router->get('/view/{id}/do/settings', [
'as' => 'admin.nodes.view.settings',
'uses' => 'Admin\NodesController@viewSettings',
]);
$router->delete('/view/{id}/deallocate/single/{allocation}', [
'uses' => 'Admin\NodesController@deallocateSingle',
$router->get('/view/{id}/do/configuration', [
'as' => 'admin.nodes.view.configuration',
'uses' => 'Admin\NodesController@viewConfiguration',
]);
$router->post('/view/{id}/deallocate/block', [
'uses' => 'Admin\NodesController@deallocateBlock',
$router->get('/view/{id}/do/allocation', [
'as' => 'admin.nodes.view.allocation',
'uses' => 'Admin\NodesController@viewAllocation',
]);
$router->post('/view/{id}/alias', [
'as' => 'admin.nodes.alias',
'uses' => 'Admin\NodesController@setAlias',
$router->post('/view/{id}/do/allocation', [
'uses' => 'Admin\NodesController@createAllocation',
]);
$router->get('/view/{id}/do/servers', [
'as' => 'admin.nodes.view.servers',
'uses' => 'Admin\NodesController@viewServers',
]);
$router->get('/view/{id}/do/delete', [
'as' => 'admin.nodes.view.delete',
'uses' => 'Admin\NodesController@viewDelete',
]);
$router->delete('/view/{id}/do/allocation/remove/{allocation}', [
'as' => 'admin.nodes.view.allocation.removeSingle',
'uses' => 'Admin\NodesController@allocationRemoveSingle',
]);
$router->post('/view/{id}/do/allocation/remove', [
'as' => 'admin.nodes.view.allocation.removeBlock',
'uses' => 'Admin\NodesController@allocationRemoveBlock',
]);
$router->post('/view/{id}/do/allocation/alias', [
'as' => 'admin.nodes.view.allocation.setAlias',
'uses' => 'Admin\NodesController@allocationSetAlias',
]);
$router->get('/view/{id}/allocations.json', [

View file

@ -27,10 +27,11 @@ namespace Pterodactyl\Models;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Nicolaslopezj\Searchable\SearchableTrait;
class Node extends Model
{
use Notifiable;
use Notifiable, SearchableTrait;
/**
* The table associated with the model.
@ -74,6 +75,18 @@ class Node extends Model
'daemonSFTP', 'daemonListen',
];
protected $searchable = [
'columns' => [
'nodes.name' => 10,
'nodes.fqdn' => 8,
'locations.short' => 4,
'locations.long' => 4,
],
'joins' => [
'locations' => ['locations.id', 'nodes.location_id'],
],
];
/**
* Return an instance of the Guzzle client for this specific node.
*

View file

@ -177,80 +177,70 @@ class NodeRepository
}
}
public function addAllocations($id, array $allocations)
/**
* Adds allocations to a provided node.
* @param integer $id
* @param array $data
*/
public function addAllocations($id, array $data)
{
$node = Models\Node::findOrFail($id);
DB::beginTransaction();
$validator = Validator::make($data, [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|required|string|max:255',
'allocation_ports' => 'required|array',
]);
try {
foreach ($allocations as $rawIP => $ports) {
try {
$setAlias = null;
$parsedIP = Network::parse($rawIP);
} catch (\Exception $ex) {
try {
$setAlias = $rawIP;
$parsedIP = Network::parse(gethostbyname($rawIP));
} catch (\Exception $ex) {
throw $ex;
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
$explode = explode('/', $data['allocation_ip']);
if (count($explode) !== 1) {
if (! ctype_digit($explode[1]) || ($explode[1] > 32 || $explode[1] < 25)) {
throw new DisplayException('CIDR notation only allows masks between /32 and /25.');
}
}
DB::transaction(function () use ($parsed, $node, $data) {
foreach(Network::parse(gethostbyname($data['allocation_ip'])) as $ip) {
foreach ($data['allocation_ports'] as $port) {
// Determine if this is a valid single port, or a valid port range.
if (! ctype_digit($port) && ! preg_match('/^(\d{1,5})-(\d{1,5})$/', $port)) {
throw new DisplayException('The mapping for <code>' . $port . '</code> is invalid and cannot be processed.');
}
}
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)) {
$block = range($matches[1], $matches[2]);
if (count($block) > 1000) {
throw new DisplayException('Adding more than 1000 ports at once is not supported. Please use a smaller port range.');
}
if (preg_match('/^(\d{1,5})-(\d{1,5})$/', $port, $matches)) {
$portBlock = range($matches[1], $matches[2]);
if (count($portBlock) > 2000) {
throw new DisplayException('Adding more than 2000 ports at once is not currently supported. Please consider using a smaller port range.');
}
foreach ($portBlock as $assignPort) {
$alloc = Models\Allocation::firstOrNew([
'node_id' => $node->id,
'ip' => $ip,
'port' => $assignPort,
]);
if (! $alloc->exists) {
$alloc->fill([
'node_id' => $node->id,
'ip' => $ip,
'port' => $assignPort,
'ip_alias' => $setAlias,
'server_id' => null,
]);
$alloc->save();
}
}
} else {
$alloc = Models\Allocation::firstOrNew([
foreach ($block as $unit) {
// Insert into Database
Models\Allocation::firstOrCreate([
'node_id' => $node->id,
'ip' => $ip,
'port' => $port,
'port' => $unit,
'ip_alias' => isset($data['allocation_alias']) ? $data['allocation_alias'] : null,
'server_id' => null,
]);
if (! $alloc->exists) {
$alloc->fill([
'node_id' => $node->id,
'ip' => $ip,
'port' => $port,
'ip_alias' => $setAlias,
'server_id' => null,
]);
$alloc->save();
}
}
} else {
// Insert into Database
Models\Allocation::firstOrCreate([
'node_id' => $node->id,
'ip' => $ip,
'port' => $port,
'ip_alias' => isset($data['allocation_alias']) ? $data['allocation_alias'] : null,
'server_id' => null,
]);
}
}
}
DB::commit();
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
});
}
public function delete($id)

File diff suppressed because one or more lines are too long

View file

@ -114,7 +114,7 @@ td.has-progress {
.input-loader {
display: none;
position:relative;
top: -23px;
top: -25px;
float: right;
right: 5px;
color: #cccccc;
@ -141,6 +141,10 @@ li.select2-results__option--highlighted[aria-selected="false"] > .user-block > .
color: #eee;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
margin: 2.5px;
}
.select2-search--inline .select2-search__field:focus {
outline: none;
border: 0 !important;
@ -224,3 +228,20 @@ span[aria-labelledby="select2-pUserId-container"] {
.nav-tabs-custom > .nav-tabs > li.active.tab-warning, .nav-tabs-custom > .nav-tabs > li.tab-warning:hover {
border-top-color: #c87f0a;
}
.nav-tabs-custom.nav-tabs-floating > .nav-tabs {
border-bottom: 0px !important;
}
.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li {
margin-bottom: 0px !important;
}
.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child.active,
.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child:hover {
border-radius: 3px 0 0 0;
}
.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child.active > a {
border-radius: 0 0 0 3px;
}

View file

@ -0,0 +1,111 @@
// Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>
//
// 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.
(function initSocket() {
if (typeof $.notifyDefaults !== 'function') {
console.error('Notify does not appear to be loaded.');
return;
}
if (typeof io !== 'function') {
console.error('Socket.io is reqired to use this panel.');
return;
}
$.notifyDefaults({
placement: {
from: 'bottom',
align: 'right'
},
newest_on_top: true,
delay: 2000,
animate: {
enter: 'animated zoomInDown',
exit: 'animated zoomOutDown'
}
});
var notifySocketError = false;
// Main Socket Object
window.Socket = io(Pterodactyl.node.scheme + '://' + Pterodactyl.node.fqdn + ':' + Pterodactyl.node.daemonListen + '/stats/', {
'query': 'token=' + Pterodactyl.node.daemonSecret,
});
// Socket Failed to Connect
Socket.io.on('connect_error', function (err) {
if(typeof notifySocketError !== 'object') {
notifySocketError = $.notify({
message: 'There was an error attempting to establish a WebSocket connection to the Daemon. This panel will not work as expected.<br /><br />' + err,
}, {
type: 'danger',
delay: 0
});
}
});
// Connected to Socket Successfully
Socket.on('connect', function () {
if (notifySocketError !== false) {
notifySocketError.close();
notifySocketError = false;
}
});
Socket.on('error', function (err) {
console.error('There was an error while attemping to connect to the websocket: ' + err + '\n\nPlease try loading this page again.');
});
Socket.on('live-stats', function (data) {
$.each(data.servers, function (uuid, info) {
var element = $('tr[data-server="' + uuid + '"]');
switch (info.status) {
case 0:
element.find('[data-action="status"]').html('<span class="label label-danger">Offline</span>');
break;
case 1:
element.find('[data-action="status"]').html('<span class="label label-success">Online</span>');
break;
case 2:
element.find('[data-action="status"]').html('<span class="label label-info">Starting</span>');
break;
case 3:
element.find('[data-action="status"]').html('<span class="label label-info">Stopping</span>');
break;
case 20:
element.find('[data-action="status"]').html('<span class="label label-warning">Installing</span>');
break;
case 30:
element.find('[data-action="status"]').html('<span class="label label-warning">Suspended</span>');
break;
}
if (info.status !== 0) {
var cpuMax = element.find('[data-action="cpu"]').data('cpumax');
var currentCpu = info.proc.cpu.total;
if (cpuMax !== 0) {
currentCpu = parseFloat(((info.proc.cpu.total / cpuMax) * 100).toFixed(2).toString());
}
element.find('[data-action="memory"]').html(parseInt(info.proc.memory.total / (1024 * 1024)));
element.find('[data-action="cpu"]').html(currentCpu);
} else {
element.find('[data-action="memory"]').html('--');
element.find('[data-action="cpu"]').html('--');
}
});
});
})();

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,118 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
List Nodes
@endsection
@section('scripts')
@parent
{!! Theme::css('vendor/fontawesome/animation.min.css') !!}
@endsection
@section('content-header')
<h1>Nodes<small>All nodes available on the system.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Nodes</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Node List</h3>
<div class="box-tools">
<form action="{{ route('admin.nodes') }}" method="GET">
<div class="input-group input-group-sm" style="width: 300px;">
<input type="text" name="query" class="form-control pull-right" value="{{ request()->input('query') }}" placeholder="Search Nodes">
<div class="input-group-btn">
<button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button>
<a href="{{ route('admin.nodes.new') }}"><button type="button" class="btn btn-sm btn-primary" style="border-radius: 0 3px 3px 0;margin-left:-1px;">Create New</button></a>
</div>
</div>
</form>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tbody>
<tr>
<th></th>
<th>Name</th>
<th>Location</th>
<th>Memory</th>
<th>Disk</th>
<th class="text-center">Servers</th>
<th class="text-center">SSL</th>
<th class="text-center">Public</th>
</tr>
@foreach ($nodes as $node)
<tr>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->daemonSecret }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</td>
<td>{{ $node->location->short }}</td>
<td>{{ $node->memory }} MB</td>
<td>{{ $node->disk }} MB</td>
<td class="text-center">{{ $node->servers_count }}</td>
<td class="text-center" style="color:{{ ($node->scheme === 'https') ? '#50af51' : '#d9534f' }}"><i class="fa fa-{{ ($node->scheme === 'https') ? 'lock' : 'unlock' }}"></i></td>
<td class="text-center"><i class="fa fa-{{ ($node->public) ? 'eye' : 'eye-slash' }}"></i></td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="box-footer with-border">
<div class="col-md-12 text-center">{!! $nodes->render() !!}</div>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
(function pingNodes() {
$('td[data-action="ping"]').each(function(i, element) {
$.ajax({
type: 'GET',
url: $(element).data('location'),
headers: {
'X-Access-Token': $(element).data('secret'),
},
timeout: 5000
}).done(function (data) {
$(element).find('i').tooltip({
title: 'v' + data.version,
});
$(element).removeClass('text-muted').find('i').removeClass().addClass('fa fa-fw fa-heartbeat faa-pulse animated').css('color', '#50af51');
}).fail(function () {
$(element).removeClass('text-muted').find('i').removeClass().addClass('fa fa-fw fa-heart-o').css('color', '#d9534f');
});
}).promise().done(function () {
setTimeout(pingNodes, 10000);
});
})();
</script>
@endsection

View file

@ -0,0 +1,245 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
{{ $node->name }}: Allocations
@endsection
@section('content-header')
<h1>{{ $node->name }}<small>Control allocations available for servers on this node.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
<li class="active">Allocations</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
<li><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
<li class="active"><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
<li class="tab-danger"><a href="{{ route('admin.nodes.view.delete', $node->id) }}">Delete</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Existing Allocations</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover" style="margin-bottom:0;">
<tr>
<th>IP Address <i class="fa fa-fw fa-minus-square" style="font-weight:normal;color:#d9534f;cursor:pointer;" data-toggle="modal" data-target="#allocationModal"></i></th>
<th>IP Alias</th>
<th>Port</th>
<th>Assigned To</th>
<th></th>
</tr>
@foreach($node->allocations as $allocation)
<tr>
<td class="col-sm-3 middle">{{ $allocation->ip }}</td>
<td class="col-sm-3 middle">
<input class="form-control input-sm" type="text" value="{{ $allocation->ip_alias }}" data-action="set-alias" data-id="{{ $allocation->id }}" placeholder="none" />
<span class="input-loader"><i class="fa fa-refresh fa-spin fa-fw"></i></span>
</td>
<td class="col-sm-2 middle">{{ $allocation->port }}</td>
<td class="col-sm-3 middle">
@if(! is_null($allocation->server))
<a href="{{ route('admin.servers.view', $allocation->server_id) }}">{{ $allocation->server->name }}</a>
@endif
</td>
<td class="col-sm-1 middle">
@if(is_null($allocation->server_id))
<button data-action="deallocate" data-id="{{ $allocation->id }}" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
@else
<button class="btn btn-sm disabled"><i class="fa fa-trash-o"></i></button>
@endif
</td>
</tr>
@endforeach
</table>
</div>
<div class="box-footer text-center">
{{ $node->allocations->render() }}
</div>
</div>
</div>
<div class="col-sm-4">
<form action="{{ route('admin.nodes.view.allocation', $node->id) }}" method="POST">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">Assign New Allocations</h3>
</div>
<div class="box-body">
<div class="form-group">
<label for="pAllocationIP" class="control-label">IP Address</label>
<div>
<select class="form-control" name="allocation_ip" id="pAllocationIP" multiple>
@foreach($node->allocations->unique('ip')->values()->all() as $allocation)
<option value="{{ $allocation->ip }}">{{ $allocation->ip }}</option>
@endforeach
</select>
<p class="text-muted small">Enter an IP address to assign ports to here.</p>
</div>
</div>
<div class="form-group">
<label for="pAllocationIP" class="control-label">IP Alias</label>
<div>
<input type="text" id="pAllocationAlias" class="form-control" name="allocation_alias" placeholder="alias" />
<p class="text-muted small">If you would like to assign a default alias to these allocations enter it here.</p>
</div>
</div>
<div class="form-group">
<label for="pAllocationPorts" class="control-label">Ports</label>
<div>
<select class="form-control" name="allocation_ports[]" id="pAllocationPorts" multiple></select>
<p class="text-muted small">Enter individual ports or port ranges here separated by commas or spaces.</p>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-success btn-sm pull-right">Submit</button>
</div>
</div>
</form>
</div>
</div>
<div class="modal fade" id="allocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Delete Allocations for IP Block</h4>
</div>
<form action="{{ route('admin.nodes.view.allocation.removeBlock', $node->id) }}" method="POST">
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<select class="form-control" name="ip">
@foreach($node->allocations->unique('ip')->values()->all() as $allocation)
<option value="{{ $allocation->ip }}">{{ $allocation->ip }}</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="modal-footer">
{{{ csrf_field() }}}
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-danger">Delete Allocations</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$('#pAllocationIP').select2({
tags: true,
maximumSelectionLength: 1,
tokenSeparators: [',', ' '],
});
$('#pAllocationPorts').select2({
tags: true,
tokenSeparators: [',', ' '],
});
$('button[data-action="deallocate"]').click(function (event) {
event.preventDefault();
var element = $(this);
var allocation = $(this).data('id');
swal({
title: '',
text: 'Are you sure you want to delete this allocation?',
type: 'warning',
showCancelButton: true,
allowOutsideClick: true,
closeOnConfirm: false,
confirmButtonText: 'Delete',
confirmButtonColor: '#d9534f',
showLoaderOnConfirm: true
}, function () {
$.ajax({
method: 'DELETE',
url: Router.route('admin.nodes.view.allocation.removeSingle', { id: Pterodactyl.node.id, allocation: allocation }),
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
}).done(function (data) {
element.parent().parent().addClass('warning').delay(100).fadeOut();
swal({ type: 'success', title: 'Port Deleted!' });
}).fail(function (jqXHR) {
console.error(jqXHR);
swal({
title: 'Whoops!',
text: jqXHR.responseJSON.error,
type: 'error'
});
});
});
});
var typingTimer;
$('input[data-action="set-alias"]').keyup(function () {
clearTimeout(typingTimer);
$(this).parent().removeClass('has-error has-success');
typingTimer = setTimeout(sendAlias, 250, $(this));
});
var fadeTimers = [];
function sendAlias(element) {
element.parent().find('.input-loader').show();
clearTimeout(fadeTimers[element.data('id')]);
$.ajax({
method: 'POST',
url: Router.route('admin.nodes.view.allocation.setAlias', { id: Pterodactyl.node.id }),
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
data: {
alias: element.val(),
allocation_id: element.data('id'),
}
}).done(function (data) {
element.parent().addClass('has-success');
}).fail(function (jqXHR) {
console.error(jqXHR);
element.parent().addClass('has-error');
}).always(function () {
element.parent().find('.input-loader').hide();
fadeTimers[element.data('id')] = setTimeout(clearHighlight, 2500, element);
});
}
function clearHighlight(element) {
element.parent().removeClass('has-error has-success');
}
</script>
@endsection

View file

@ -0,0 +1,102 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
{{ $node->name }}: Configuration
@endsection
@section('content-header')
<h1>{{ $node->name }}<small>Your daemon configuration file.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
<li class="active">Configuration</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
<li><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
<li class="active"><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
<li class="tab-danger"><a href="{{ route('admin.nodes.view.delete', $node->id) }}">Delete</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Configuration File</h3>
</div>
<div class="box-body">
<pre class="no-margin">{{ $node->getConfigurationAsJson(true) }}</pre>
</div>
<div class="box-footer">
<p class="no-margin">This file should be placed in your daemon's <code>config</code> directory in a file called <code>core.json</code>.</p>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">Auto-Deploy</h3>
</div>
<div class="box-body">
<p class="text-muted small">To simplify the configuration of nodes it is possible to fetch the config from the panel. A token is required for this process. The button below will generate a token and provide you with the commands necessary for automatic configuration of the node. <em>Tokens are only valid for 5 minutes.</em></p>
</div>
<div class="box-footer">
<button type="button" id="configTokenBtn" class="btn btn-sm btn-default" style="width:100%;">Generate Token</button>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$('#configTokenBtn').on('click', function (event) {
$.getJSON('{{ route('admin.nodes.configuration-token', $node->id) }}').done(function (data) {
swal({
type: 'success',
title: 'Token created.',
text: 'Your token will expire <strong>in 5 minutes.</strong><br /><br />' +
'<p>To auto-configure your node run the following command:<br /><small><pre>npm run configure -- --panel-url {{ config('app.url') }} --token ' + data.token + '</pre></small></p>',
html: true
})
}).fail(function () {
swal({
title: 'Error',
text: 'Something went wrong creating your token.',
type: 'error'
});
});
});
</script>
@endsection

View file

@ -0,0 +1,143 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
{{ $node->name }}
@endsection
@section('content-header')
<h1>{{ $node->name }}<small>A quick overview of your node.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
<li class="active">{{ $node->name }}</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li class="active"><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
<li><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
<li class="tab-danger"><a href="{{ route('admin.nodes.view.delete', $node->id) }}">Delete</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Information</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<td>Daemon Version</td>
<td><code data-attr="info-version"><i class="fa fa-refresh fa-fw fa-spin"></i></code> (Latest: <code>{{ Version::getDaemon() }}</code>)</td>
</tr>
<tr>
<td>System Information</td>
<td data-attr="info-system"><i class="fa fa-refresh fa-fw fa-spin"></i></td>
</tr>
<tr>
<td>Total CPU Cores</td>
<td data-attr="info-cpus"><i class="fa fa-refresh fa-fw fa-spin"></i></td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">At-a-Glance</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-12">
<div class="info-box bg-{{ $stats['disk']['css'] }}">
<span class="info-box-icon"><i class="ion ion-ios-folder-outline"></i></span>
<div class="info-box-content" style="padding: 15px 10px 0;">
<span class="info-box-text">Disk Space Allocated</span>
<span class="info-box-number">{{ $stats['disk']['value'] }} Mb</span>
<div class="progress">
<div class="progress-bar" style="width: {{ $stats['disk']['percent'] }}%"></div>
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="info-box bg-{{ $stats['memory']['css'] }}">
<span class="info-box-icon"><i class="ion ion-ios-barcode-outline"></i></span>
<div class="info-box-content" style="padding: 15px 10px 0;">
<span class="info-box-text">Memory Allocated</span>
<span class="info-box-number">{{ $stats['memory']['value'] }} Mb</span>
<div class="progress">
<div class="progress-bar" style="width: {{ $stats['memory']['percent'] }}%"></div>
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="info-box bg-gray">
<span class="info-box-icon"><i class="ion ion-social-buffer-outline"></i></span>
<div class="info-box-content" style="padding: 23px 10px 0;">
<span class="info-box-text">Total Servers</span>
<span class="info-box-number">{{ $node->servers_count }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
(function getInformation() {
$.ajax({
method: 'GET',
url: '{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}',
timeout: 5000,
headers: {
'X-Access-Token': '{{ $node->daemonSecret }}'
},
}).done(function (data) {
$('[data-attr="info-version"]').html(data.version);
$('[data-attr="info-system"]').html(data.system.type + '(' + data.system.arch + ') <code>' + data.system.release + '</code>');
$('[data-attr="info-cpus"]').html(data.system.cpus);
}).fail(function (jqXHR) {
}).always(function() {
setTimeout(getInformation, 10000);
});
})();
</script>
@endsection

View file

@ -0,0 +1,91 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
{{ $node->name }}: Servers
@endsection
@section('content-header')
<h1>{{ $node->name }}<small>All servers currently assigned to this node.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
<li class="active">Servers</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
<li><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
<li class="active"><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
<li class="tab-danger"><a href="{{ route('admin.nodes.view.delete', $node->id) }}">Delete</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Process Manager</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Server Name</th>
<th>Owner</th>
<th>Service</th>
<th class="text-center">Memory</th>
<th class="text-center">Disk</th>
<th class="text-center">CPU</th>
<th class="text-center">Status</th>
</tr>
@foreach($node->servers as $server)
<tr data-server="{{ $server->uuid }}">
<td><code>{{ $server->uuidShort }}</code></td>
<td><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></td>
<td><a href="{{ route('admin.users.view', $server->owner_id) }}"><code>{{ $server->user->email }}</code></a></td>
<td>{{ $server->service->name }}</td>
<td class="text-center"><span data-action="memory">NaN</span> / {{ $server->memory === 0 ? '&infin;' : $server->memory }} MB</td>
<td class="text-center">{{ $server->disk }} MB</td>
<td class="text-center"><span data-action="cpu" data-cpumax="{{ $server->cpu }}">NaN</span> %</td>
<td class="text-center" data-action="status">NaN</td>
</tr>
@endforeach
</table>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('js/admin/node/view-servers.js') !!}
@endsection

View file

@ -0,0 +1,224 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- 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. --}}
@extends('layouts.admin')
@section('title')
{{ $node->name }}: Settings
@endsection
@section('content-header')
<h1>{{ $node->name }}<small>Configure your node settings.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
<li class="active"><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
<li class="tab-danger"><a href="{{ route('admin.nodes.view.delete', $node->id) }}">Delete</a></li>
</ul>
</div>
</div>
</div>
<form action="{{ route('admin.nodes.view.settings', $node->id) }}" method="POST">
<div class="row">
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Settings</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="name" class="control-label">Node Name</label>
<div>
<input type="text" autocomplete="off" name="name" class="form-control" value="{{ old('name', $node->name) }}" />
<p class="text-muted"><small>Character limits: <code>a-zA-Z0-9_.-</code> and <code>[Space]</code> (min 1, max 100 characters).</small></p>
</div>
</div>
<div class="form-group col-xs-12">
<label for="name" class="control-label">Location</label>
<div>
<select name="location_id" class="form-control">
@foreach($locations as $location)
<option value="{{ $location->id }}" {{ (old('location_id', $node->location_id) === $location->id) ? 'selected' : '' }}>{{ $location->long }} ({{ $location->short }})</option>
@endforeach
</select>
</div>
</div>
<div class="form-group col-xs-12">
<label for="public" class="control-label">Allow Automatic Allocation <sup><a data-toggle="tooltip" data-placement="top" title="Allow automatic allocation to this Node?">?</a></sup></label>
<div>
<input type="radio" name="public" value="1" {{ (old('public', $node->public) === '1') ? 'checked' : '' }} id="public_1" checked> <label for="public_1" style="padding-left:5px;">Yes</label><br />
<input type="radio" name="public" value="0" {{ (old('public', $node->public) === '0') ? 'checked' : '' }} id="public_0"> <label for="public_0" style="padding-left:5px;">No</label>
</div>
</div>
<div class="form-group col-xs-12">
<label for="fqdn" class="control-label">Fully Qualified Domain Name</label>
<div>
<input type="text" autocomplete="off" name="fqdn" class="form-control" value="{{ old('fqdn', $node->fqdn) }}" />
</div>
<p class="text-muted"><small>Please enter domain name (e.g <code>node.example.com</code>) to be used for connecting to the daemon. An IP address may only be used if you are not using SSL for this node.
<a tabindex="0" data-toggle="popover" data-trigger="focus" title="Why do I need a FQDN?" data-content="In order to secure communications between your server and this node we use SSL. We cannot generate a SSL certificate for IP Addresses, and as such you will need to provide a FQDN.">Why?</a>
</small></p>
</div>
<div class="form-group col-xs-12">
<label for="scheme" class="control-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Secure Socket Layer</label>
<div class="row" style="padding: 7px 0;">
<div class="col-xs-6">
<input type="radio" name="scheme" value="https" id="scheme_ssl" {{ (old('scheme', $node->scheme) === 'https') ? 'checked' : '' }}/> <label for="scheme_ssl" style="padding-left: 5px;">Enable HTTPS/SSL</label>
</div>
<div class="col-xs-6">
<input type="radio" name="scheme" value="http" id="scheme_nossl" {{ (old('scheme', $node->scheme) === 'http') ? 'checked' : '' }}/> <label for="scheme_nossl" style="padding-left: 5px;">Disable HTTPS/SSL</label>
</div>
</div>
<p class="text-muted"><small>You should always leave SSL enabled for nodes. Disabling SSL could allow a malicious user to intercept traffic between the panel and the daemon potentially exposing sensitive information.</small></p>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Allocation Limits</h3>
</div>
<div class="box-body row">
<div class="col-xs-12">
<div class="row">
<div class="form-group col-xs-6">
<label for="memory" class="control-label">Total Memory</label>
<div class="input-group">
<input type="text" name="memory" class="form-control" data-multiplicator="true" value="{{ old('memory', $node->memory) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="form-group col-xs-6">
<label for="memory_overallocate" class="control-label">Overallocate</label>
<div class="input-group">
<input type="text" name="memory_overallocate" class="form-control" value="{{ old('memory_overallocate', $node->memory_overallocate) }}"/>
<span class="input-group-addon">%</span>
</div>
</div>
</div>
<p class="text-muted small">Enter the total amount of memory available on this node for allocation to servers. You may also provide a percentage that can allow allocation of more than the defined memory.</p>
</div>
<div class="col-xs-12">
<div class="row">
<div class="form-group col-xs-6">
<label for="disk" class="control-label">Disk Space</label>
<div class="input-group">
<input type="text" name="disk" class="form-control" data-multiplicator="true" value="{{ old('disk', $node->disk) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="form-group col-xs-6">
<label for="disk_overallocate" class="control-label">Overallocate</label>
<div class="input-group">
<input type="text" name="disk_overallocate" class="form-control" value="{{ old('disk_overallocate', $node->disk_overallocate) }}"/>
<span class="input-group-addon">%</span>
</div>
</div>
</div>
<p class="text-muted small">Enter the total amount of disk space available on this node for server allocation. You may also provide a percentage that will determine the amount of disk space over the set limit to allow.</p>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">General Configuration</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="disk_overallocate" class="control-label">Maximum Web Upload Filesize</label>
<div class="input-group">
<input type="text" name="upload_size" class="form-control" value="{{ old('upload_size', $node->upload_size) }}"/>
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted"><small>Enter the maximum size of files that can be uploaded through the web-based file manager.</small></p>
</div>
<div class="col-xs-12">
<div class="row">
<div class="form-group col-md-6">
<label for="daemonListen" class="control-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Daemon Port</label>
<div>
<input type="text" name="daemonListen" class="form-control" value="{{ old('daemonListen', $node->daemonListen) }}"/>
</div>
</div>
<div class="form-group col-md-6">
<label for="daemonSFTP" class="control-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Daemon SFTP Port</label>
<div>
<input type="text" name="daemonSFTP" class="form-control" value="{{ old('daemonSFTP', $node->daemonSFTP) }}"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p class="text-muted"><small>The daemon runs its own SFTP management container and does not use the SSHd process on the main physical server. <Strong>Do not use the same port that you have assigned for your physcial server's SSH process.</strong></small></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Save Settings</h3>
</div>
<div class="box-body row">
<div class="form-group col-sm-6">
<div>
<input type="checkbox" name="reset_secret" id="reset_secret" /> <label for="reset_secret" class="control-label">Reset Daemon Master Key</label>
</div>
<p class="text-muted"><small>Resetting the daemon master key will void any request coming from the old key. This key is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this key regularly for security.</small></p>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-primary pull-right">Save Changes</button>
</div>
</div>
</div>
</div>
</form>
@endsection
@section('footer-scripts')
@parent
<script>
$('[data-toggle="popover"]').popover({
placement: 'auto'
});
$('select[name="location_id"]').select2();
</script>
@endsection

View file

@ -40,7 +40,7 @@
<div class="box-tools">
<form action="{{ route('admin.servers') }}" method="GET">
<div class="input-group input-group-sm" style="width: 300px;">
<input type="text" name="query" class="form-control pull-right" value="{{ request()->input('query') }}" placeholder="Search">
<input type="text" name="query" class="form-control pull-right" value="{{ request()->input('query') }}" placeholder="Search Servers">
<div class="input-group-btn">
<button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button>
<a href="{{ route('admin.servers.new') }}"><button type="button" class="btn btn-sm btn-primary" style="border-radius: 0 3px 3px 0;margin-left:-1px;">Create New</button></a>

View file

@ -90,15 +90,15 @@
<i class="fa fa-wrench"></i> <span>Settings</span>
</a>
</li>
<li class="header">SERVER MANAGEMENT</li>
<li class="header">MANAGEMENT</li>
<li class="{{ Route::currentRouteName() !== 'admin.servers' ?: 'active' }}">
<a href="{{ route('admin.servers') }}">
<i class="fa fa-server"></i> <span>List Servers</span>
</a>
</li>
<li class="{{ Route::currentRouteName() !== 'admin.servers.new' ?: 'active' }}">
<a href="{{ route('admin.servers.new') }}">
<i class="fa fa-plus"></i> <span>Create Server</span>
<li class="{{ Route::currentRouteName() !== 'admin.nodes' ?: 'active' }}">
<a href="{{ route('admin.nodes') }}">
<i class="fa fa-sitemap"></i> <span>List Nodes</span>
</a>
</li>
</ul>