Define Port Groups (#12402)

* Define Port Groups

* .

* .

* .

* API Calls

* .

* .

* .

* minor changes

* .

* update forms

* remove link

* .

* change column settings

* change migration

* change update position

* db migration fix

* .

* .

* .

* add missing doc reference

* update test data

* update test data

* update test data

* .

* .

* .

* .

* .

* .

* .

* .

* port group association in seperate table

* .

* .

* show all found groups on port

* select multiple Portgroups per Port

* change on migration file

* change query to eloquent

* Code changes

* move port group menu to ports main menu

* port group update to eloquent

* .

* .

* update to new setting way

* add missing merge parameter

* Use select2 and port some things to Laravel
some fixes, hopefully no new added bugs

* schema

* don't use on update restrict unfortunately

* remove unused import and revert changes

Co-authored-by: Tony Murray <murraytony@gmail.com>
This commit is contained in:
SourceDoctor 2021-04-07 00:25:08 +02:00 committed by GitHub
parent ecb87d9671
commit 4b9e480118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 875 additions and 174 deletions

View File

@ -0,0 +1,57 @@
<?php
/*
* PortController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers;
use App\Models\Port;
class PortController extends Controller
{
public function update(\Illuminate\Http\Request $request, Port $port)
{
$this->validate($request, [
'groups.*' => 'int',
]);
$updated = false;
$message = '';
if ($request->has('groups')) {
$changes = $port->groups()->sync($request->get('groups'));
$groups_updated = array_sum(array_map(function ($group_ids) {
return count($group_ids);
}, $changes));
if ($groups_updated > 0) {
$message .= trans('port.groups.updated', ['port' => $port->getLabel()]);
$updated = true;
}
}
return $updated
? response(['message' => $message])
: response(['message' => trans('port.groups.none', ['port' => $port->getLabel()])], 400);
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers;
use App\Models\PortGroup;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Toastr;
class PortGroupController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('port-group.index', [
'port_groups' => PortGroup::orderBy('name')->get(),
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('port-group.create', [
'port_group' => new PortGroup(),
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|string|unique:port_groups',
]);
$portGroup = PortGroup::make($request->only(['name', 'desc']));
$portGroup->save();
Toastr::success(__('Port Group :name created', ['name' => $portGroup->name]));
return redirect()->route('port-groups.index');
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\PortGroup $portGroup
* @return \Illuminate\Http\Response
*/
public function edit(PortGroup $portGroup)
{
return view('port-group.edit', [
'port_group' => $portGroup,
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\PortGroup $portGroup
* @return \Illuminate\Http\Response
*/
public function update(Request $request, PortGroup $portGroup)
{
$this->validate($request, [
'name' => [
'required',
'string',
'max:255',
Rule::unique('port_groups', 'name')->where(function ($query) use ($portGroup) {
$query->where('id', '!=', $portGroup->id);
}),
],
'desc' => 'string|max:255',
]);
$portGroup->fill($request->only(['name', 'desc']));
if ($portGroup->save()) {
Toastr::success(__('Port Group :name updated', ['name' => $portGroup->name]));
} else {
Toastr::error(__('Failed to save'));
return redirect()->back()->withInput();
}
return redirect()->route('port-groups.index');
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\PortGroup $portGroup
* @return \Illuminate\Http\Response
*/
public function destroy(PortGroup $portGroup)
{
$portGroup->delete();
$msg = __('Port Group :name deleted', ['name' => $portGroup->name]);
return response($msg, 200);
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* PortGroupController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @link http://librenms.org
* @copyright 2020 Thomas Berberich
* @author Thomas Berberich <sourcehhdoctor@gmail.com>
*/
namespace App\Http\Controllers\Select;
use App\Models\PortGroup;
class PortGroupController extends SelectController
{
protected function searchFields($request)
{
return ['name'];
}
protected function baseQuery($request)
{
return PortGroup::hasAccess($request->user())->select(['id', 'name']);
}
public function formatItem($port_group)
{
return [
'id' => $port_group->id,
'text' => $port_group->name,
];
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* EditPortsController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers\Table;
class EditPortsController extends TableController
{
public function rules()
{
return [
'device_id' => 'required|int',
'device_group' => 'nullable|int',
'eventtype' => 'nullable|string',
];
}
public function searchFields($request)
{
return ['ifName', 'ifAlias', 'ifDescr'];
}
protected function sortFields($request)
{
return ['ifIndex', 'ifName', 'ifAdminStatus', 'ifOperStatus', 'ifSpeed', 'ifAlias'];
}
protected function baseQuery($request)
{
return \App\Models\Port::where('device_id', $request->get('device_id'))
->with('groups');
}
/**
* @param \App\Models\Port $port
* @return array
*/
public function formatItem($port)
{
$is_port_bad = $port->ifAdminStatus != 'down' && $port->ifOperStatus != 'up';
$do_we_care = ($port->ignore || $port->disabled) ? false : $is_port_bad;
$out_of_sync = $do_we_care ? "class='red'" : '';
$tune = $port->device->getAttrib('ifName_tune:' . $port->ifName) == 'true' ? 'checked' : '';
$port_group_options = '';
foreach ($port->groups as $group) {
/** @var \App\Models\PortGroup $group */
$port_group_options .= '<option value="' . $group->id . '" selected>' . $group->name . '</option>';
}
return [
'ifIndex' => $port->ifIndex,
'ifName' => $port->getLabel(),
'ifAdminStatus' => $port->ifAdminStatus,
'ifOperStatus' => '<span id="operstatus_' . $port->port_id . '" ' . $out_of_sync . '>' . $port->ifOperStatus . '</span>',
'disabled' => '<input type="checkbox" class="disable-check" data-size="small" name="disabled_' . $port->port_id . '"' . ($port->disabled ? 'checked' : '') . '>
<input type="hidden" name="olddis_' . $port->port_id . '" value="' . ($port->disabled ? 1 : 0) . '"">',
'ignore' => '<input type="checkbox" class="ignore-check" data-size="small" name="ignore_' . $port->port_id . '"' . ($port->ignore ? 'checked' : '') . '>
<input type="hidden" name="oldign_' . $port->port_id . '" value="' . ($port->ignore ? 1 : 0) . '"">',
'port_tune' => '<input type="checkbox" name="override_config" data-attrib="ifName_tune:' . $port->ifName . '" data-device_id="' . $port->device_id . '" data-size="small" ' . $tune . '>',
'ifAlias' => '<div class="form-group"><input class="form-control input-sm" name="if-alias" data-device_id="' . $port->device_id . '" data-port_id="' . $port->port_id . '" data-ifName="' . $port->ifName . '" value="' . $port->ifAlias . '"><span class="form-control-feedback"><i class="fa" aria-hidden="true"></i></span></div>',
'ifSpeed' => '<div class="form-group has-feedback"><input type="text" pattern="[0-9]*" inputmode="numeric" class="form-control input-sm" name="if-speed" data-device_id="' . $port->device_id . '" data-port_id="' . $port->port_id . '" data-ifName="' . $port->ifName . '" value="' . $port->ifSpeed . '"><span class="form-control-feedback"><i class="fa" aria-hidden="true"></i></span></div>',
'portGroup' => '<div class="form-group has-feedback"><select class="input-sm port_group_select" name="port_group_' . $port->port_id . '[]" data-port_id="' . $port->port_id . '" multiple>' . $port_group_options . '</select></div>',
];
}
}

View File

@ -37,6 +37,7 @@ class TopInterfacesController extends WidgetController
'time_interval' => 15,
'interface_filter' => null,
'device_group' => null,
'port_group' => null,
];
/**
@ -60,6 +61,9 @@ class TopInterfacesController extends WidgetController
}, function ($query) {
$query->has('device');
})
->when($data['port_group'], function ($query) use ($data) {
$query->inPortGroup($data['port_group']);
})
->orderByRaw('SUM(LEAST(ifInOctets_rate, 9223372036854775807) + LEAST(ifOutOctets_rate, 9223372036854775807)) DESC')
->limit($data['interface_count']);

View File

@ -26,6 +26,7 @@ namespace App\Http\Controllers\Widgets;
use App\Http\Controllers\Controller;
use App\Models\DeviceGroup;
use App\Models\PortGroup;
use App\Models\UserWidget;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
@ -67,9 +68,22 @@ abstract class WidgetController extends Controller
// This might be invoked in getSettingsView() in an extended class
// So don't run it before since it's cached.
$this->getSettings();
}
if (! empty($this->settings['device_group'])) {
$this->title .= ' (' . DeviceGroup::find($this->settings['device_group'])->name . ')';
if (! $this->show_settings) {
if (! empty($this->settings['device_group']) || ! empty($this->settings['port_group'])) {
$this->title .= ' (';
$title_details = [];
if (! empty($this->settings['device_group'])) {
$title_details[] = DeviceGroup::find($this->settings['device_group'])->name;
}
if (! empty($this->settings['port_group'])) {
$title_details[] = PortGroup::find($this->settings['port_group'])->name;
}
$this->title .= implode(' ; ', $title_details);
$this->title .= ')';
}
$view = $this->getView($request);
}
@ -101,6 +115,10 @@ abstract class WidgetController extends Controller
if ($settingsView && isset($this->settings['device_group'])) {
$this->settings['device_group'] = DeviceGroup::find($this->settings['device_group']);
}
if ($settingsView && isset($this->settings['port_group'])) {
$this->settings['port_group'] = PortGroup::find($this->settings['port_group']);
}
}
return $this->settings;

View File

@ -255,6 +255,15 @@ class Port extends DeviceRelatedModel
return $this->hasPortAccess($query, $user);
}
public function scopeInPortGroup($query, $portGroup)
{
return $query->whereIn($query->qualifyColumn('port_id'), function ($query) use ($portGroup) {
$query->select('port_id')
->from('port_group_port')
->where('port_group_id', $portGroup);
});
}
// ---- Define Relationships ----
public function adsl(): HasMany
@ -272,6 +281,11 @@ class Port extends DeviceRelatedModel
return $this->hasMany(\App\Models\PortsFdb::class, 'port_id', 'port_id');
}
public function groups(): BelongsToMany
{
return $this->belongsToMany(\App\Models\PortGroup::class, 'port_group_port', 'port_id', 'port_group_id');
}
public function ipv4(): HasMany
{
return $this->hasMany(\App\Models\Ipv4Address::class, 'port_id');

45
app/Models/PortGroup.php Normal file
View File

@ -0,0 +1,45 @@
<?php
/**
* PortGroup.php
*
* Groups of ports
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @link http://librenms.org
* @copyright 2020 Thomas Berberich
* @author Thomas Berberich <sourcehhdoctor@gmail.com>
*/
namespace App\Models;
class PortGroup extends BaseModel
{
public $timestamps = false;
protected $fillable = ['name', 'desc'];
public function scopeHasAccess($query, User $user)
{
if ($user->hasGlobalRead()) {
return $query;
}
// maybe filtered in future
return $query;
}
public function ports()
{
return $this->belongsToMany(\App\Models\Port::class, 'port_group_port', 'port_group_id', 'port_id');
}
}

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePortGroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('port_groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->string('desc')->nullable();
});
Schema::create('port_group_port', function (Blueprint $table) {
$table->unsignedInteger('port_group_id')->unsigned()->index();
$table->unsignedInteger('port_id')->unsigned()->index();
$table->primary(['port_group_id', 'port_id']);
$table->foreign('port_group_id')->references('id')->on('port_groups')->onDelete('CASCADE');
$table->foreign('port_id')->references('port_id')->on('ports')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('port_group_port');
Schema::drop('port_groups');
}
}

71
doc/API/Port_Groups.md Normal file
View File

@ -0,0 +1,71 @@
source: API/Port_Groups.md
path: blob/master/doc/
### `get_portgroups`
List all port groups.
Route: `/api/v0/portgroups`
Input (JSON):
-
Examples:
```curl
curl -H 'X-Auth-Token: YOURAPITOKENHERE' https://librenms.org/api/v0/portgroups
```
Output:
```json
[
{
"status": "ok",
"message": "Found 1 port groups",
"count": 1,
"groups": [
{
"id": "1",
"name": "Testing",
"desc": "Testing"
}
]
}
]
```
### `add_portgroup`
Add a new port group. Upon success, the ID of the new port group is returned
and the HTTP response code is `201`.
Route: `/api/v0/port_groups`
Input (JSON):
- `name`: *required* - The name of the port group
- `desc`: *optional* - Description of the port group
Examples:
Dynamic Example:
```curl
curl -H 'X-Auth-Token: YOURAPITOKENHERE' \
-X POST \
-d '{"name": "New Port Group", \
"desc": "A very fancy port group"}' \
https://librenms.org/api/v0/port_groups
```
Output:
```json
{
"status": "ok",
"id": 86,
"message": "Port group New Port Group created"
}
```

View File

@ -74,6 +74,7 @@ Output from the API currently is via two output types:
- [Devices](Devices.md)
- [DeviceGroups](DeviceGroups.md)
- [Ports](Ports.md)
- [Port_Groups](Port_Groups.md)
- [PortGroups](PortGroups.md)
- [Alerts](Alerts.md)
- [Routing](Routing.md)

View File

@ -16,6 +16,7 @@ use App\Models\Availability;
use App\Models\Device;
use App\Models\DeviceGroup;
use App\Models\DeviceOutage;
use App\Models\PortGroup;
use App\Models\PortsFdb;
use App\Models\Sensor;
use App\Models\ServiceTemplate;
@ -1846,6 +1847,41 @@ function rename_device(Illuminate\Http\Request $request)
}
}
function add_port_group(Illuminate\Http\Request $request)
{
$data = json_decode($request->getContent(), true);
if (json_last_error() || ! is_array($data)) {
return api_error(400, "We couldn't parse the provided json. " . json_last_error_msg());
}
$rules = [
'name' => 'required|string|unique:port_groups',
];
$v = Validator::make($data, $rules);
if ($v->fails()) {
return api_error(422, $v->messages());
}
$portGroup = PortGroup::make(['name' => $data['name'], 'desc' => $data['desc']]);
$portGroup->save();
return api_success($portGroup->id, 'id', 'Port group ' . $portGroup->name . ' created', 201);
}
function get_port_groups(Illuminate\Http\Request $request)
{
$query = PortGroup::query();
$groups = $query->orderBy('name')->get();
if ($groups->isEmpty()) {
return api_error(404, 'No port groups found');
}
return api_success($groups->makeHidden('pivot')->toArray(), 'groups', 'Found ' . $groups->count() . ' port groups');
}
function add_device_group(Illuminate\Http\Request $request)
{
$data = json_decode($request->getContent(), true);

View File

@ -65,13 +65,13 @@ foreach ($_POST as $key => $val) {
}//end foreach
if ($rows_updated > 0) {
$message = $rows_updated . ' Device record updated.';
$message = $rows_updated . ' Port record(s) updated.';
$status = 'ok';
} elseif ($rows_updated = '-1') {
$message = 'Device record unchanged. No update necessary.';
$message = 'Port records unchanged. No update necessary.';
$status = 'ok';
} else {
$message = 'Device record update error.';
$message = 'Port record update error.';
}
$response = [

View File

@ -1,6 +1,6 @@
</form>
<h3> Port Settings </h3>
<span id="message"><small><div class="alert alert-danger">n.b For the first time, please click any button twice.</div></small></span>
<span id="message"></span>
<form id='ignoreport' name='ignoreport' method='post' action='' role='form' class='form-inline'>
<?php echo csrf_field() ?>
@ -14,10 +14,11 @@
<th data-column-id='ifIndex'>Index</th>
<th data-column-id='ifName'>Name</th>
<th data-column-id='ifAdminStatus'>Admin</th>
<th data-column-id='ifOperStatus'>Oper</th>
<th data-column-id='ifOperStatus'>Operational</th>
<th data-column-id='disabled' data-sortable='false'>Disable polling</th>
<th data-column-id='ignore' data-sortable='false'>Ignore alert tag</th>
<th data-column-id='ifSpeed'>ifSpeed (bits/s)</th>
<th data-column-id='portGroup' data-sortable='false' data-searchable='false'>Port Group</th>
<th data-column-id='port_tune' data-sortable='false' data-searchable='false'>RRD Tune</th>
<th data-column-id='ifAlias'>Description</th>
</tr>
@ -111,106 +112,153 @@
});
});
$(document).ready(function() {
$('form#ignoreport').submit(function (event) {
$('#disable-toggle').click(function (event) {
// invert selection on all disable buttons
event.preventDefault();
$('input[name^="disabled_"]').trigger('click');
});
$('#ignore-toggle').click(function (event) {
// invert selection on all ignore buttons
event.preventDefault();
$('input[name^="ignore_"]').trigger('click');
});
$('#disable-select').click(function (event) {
// select all disable buttons
event.preventDefault();
$('.disable-check').prop('checked', true);
//TODO: find a better solution for 'select-all' button refresh
$('.disable-check').trigger('click');
$('.disable-check').trigger('click');
});
$('#ignore-select').click(function (event) {
// select all ignore buttons
event.preventDefault();
$('.ignore-check').prop('checked', true);
//TODO: find a better solution for 'select-all' button refresh
$('.ignore-check').trigger('click');
$('.ignore-check').trigger('click');
});
$('#down-select').click(function (event) {
// select ignore buttons for all ports which are down
event.preventDefault();
$('[name^="operstatus_"]').each(function () {
var name = $(this).attr('name');
var text = $(this).text();
if (name && text == 'down') {
// get the interface number from the object name
var port_id = name.split('_')[1];
// find its corresponding checkbox and toggle it
$('input[name="ignore_' + port_id + '"]').trigger('click');
}
});
});
$('#alerted-toggle').click(function (event) {
// toggle ignore buttons for all ports which are in class red
event.preventDefault();
$('.red').each(function () {
var name = $(this).attr('name');
if (name) {
// get the interface number from the object name
var port_id = name.split('_')[1];
// find its corresponding checkbox and toggle it
$('input[name="ignore_' + port_id + '"]').trigger('click');
}
});
});
$('#form-reset').click(function (event) {
// reset objects in the form to their previous values
event.preventDefault();
$('#ignoreport')[0].reset();
});
$('#save-form').click(function (event) {
// reset objects in the form to their previous values
event.preventDefault();
$.ajax({
type: "POST",
url: "ajax_form.php",
data: $('form#ignoreport').serialize(),
dataType: "json",
success: function(data){
if (data.status == 'ok') {
$("#message").html('<div class="alert alert-info">' + data.message + '</div>')
} else {
$("#message").html('<div class="alert alert-danger">' + data.message + '</div>');
}
},
error: function(){
$("#message").html('<div class="alert alert-danger">Error creating config item</div>');
}
});
});
$('#disable-toggle').click(function (event) {
// invert selection on all disable buttons
event.preventDefault();
$('input[name^="disabled_"]').trigger('click');
});
$('#ignore-toggle').click(function (event) {
// invert selection on all ignore buttons
event.preventDefault();
$('input[name^="ignore_"]').trigger('click');
});
$('#disable-select').click(function (event) {
// select all disable buttons
event.preventDefault();
$('.disable-check').prop('checked', true);
//TODO: find a better solution for 'select-all' button refresh
$('.disable-check').trigger('click');
$('.disable-check').trigger('click');
});
$('#ignore-select').click(function (event) {
// select all ignore buttons
event.preventDefault();
$('.ignore-check').prop('checked', true);
//TODO: find a better solution for 'select-all' button refresh
$('.ignore-check').trigger('click');
$('.ignore-check').trigger('click');
});
$('#down-select').click(function (event) {
// select ignore buttons for all ports which are down
event.preventDefault();
$('[id^="operstatus_"]').each(function () {
var name = $(this).attr('name');
var text = $(this).text();
if (name && text == 'down') {
// get the interface number from the object name
var port_id = name.split('_')[1];
// find its corresponding checkbox and toggle it
$('input[name="ignore_' + port_id + '"]').trigger('click');
}
});
});
$('#alerted-toggle').click(function (event) {
// toggle ignore buttons for all ports which are in class red
event.preventDefault();
$('.red').each(function () {
var name = $(this).attr('name');
if (name) {
// get the interface number from the object name
var port_id = name.split('_')[1];
// find its corresponding checkbox and toggle it
$('input[name="ignore_' + port_id + '"]').trigger('click');
}
});
});
$('#form-reset').click(function (event) {
// reset objects in the form to their previous values
event.preventDefault();
$('#ignoreport')[0].reset();
});
$('#save-form').click(function (event) {
// reset objects in the form to their previous values
event.preventDefault();
$.ajax({
type: "POST",
url: "ajax_form.php",
data: $('form#ignoreport').serialize(),
dataType: "json",
success: function(data){
if (data.status == 'ok') {
$("#message").html('<div class="alert alert-info">' + data.message + '</div>')
} else {
$("#message").html('<div class="alert alert-danger">' + data.message + '</div>');
}
},
error: function(){
$("#message").html('<div class="alert alert-danger">Error creating config item</div>');
}
});
});
$('form#ignoreport').submit(function (event) {
event.preventDefault();
});
});
var grid = $("#edit-ports").bootgrid({
ajax: true,
rowCount: [50, 100, 250, -1],
templates: {
header: '<div id="{{ctx.id}}" class="{{css.header}}"><div class="row">\
<div class="col-sm-8 actionBar header_actions">\
<span class="pull-left">\
<span class="action_group">Disable polling\
<button type="submit" value="Toggle" class="btn btn-default btn-sm" id="disable-toggle" title="Toggle polling for all ports">Toggle</button>\
<button type="submit" value="Select" class="btn btn-default btn-sm" id="disable-select" title="Disable polling on all ports">Disable All</button>\
</span>\
<span class="action_group">Ignore alerts\
<button type="submit" value="Alerted" class="btn btn-default btn-sm" id="alerted-toggle" title="Toggle alerting on all currently-alerted ports">Alerted</button>\
<button type="submit" value="Down" class="btn btn-default btn-sm" id="down-select" title="Disable alerting on all currently-down ports">Down</button>\
<button type="submit" value="Toggle" class="btn btn-default btn-sm" id="ignore-toggle" title="Toggle alert tag for all ports">Toggle</button>\
<button type="submit" value="Select" class="btn btn-default btn-sm" id="ignore-select" title="Disable alert tag on all ports">Ignore All</button></span>\
</span>\
<span class="action_group">\
<button id="save-form" type="submit" value="Save" class="btn btn-success btn-sm" title="Save current port disable/ignore settings">Save Toggles</button>\
<button type="submit" value="Reset" class="btn btn-danger btn-sm" id="form-reset" title="Reset form to previously-saved settings">Reset</button>\
</span>\
</span>\
</div>\
<div class="col-sm-4 actionBar"><p class="{{css.search}}"></p><p class="{{css.actions}}"></p></div>\
</div></div>'
},
post: function ()
{
return {
id: 'edit-ports',
device_id: "<?php echo $device['device_id']; ?>"
};
},
url: "ajax_table.php"
url: "<?php echo url('/ajax/table/edit-ports/'); ?>"
}).on("loaded.rs.jquery.bootgrid", function() {
$("[type='checkbox']").bootstrapSwitch();
$("[name='override_config']").bootstrapSwitch('offColor','danger');
$('input[name="override_config"]').on('switchChange.bootstrapSwitch', function(event, state) {
override_config(event,state,$(this));
});
init_select2('.port_group_select', 'port-group', {}, null, 'No Group');
$('.port_group_select').on('change', function (e) {
var $target = $(e.target)
$.ajax({
type: "PUT",
url: "<?php echo url('port'); ?>/" + $target.data('port_id'),
data: {"groups": $target.val()},
success: function(data) {
toastr.success(data.message)
},
error: function(data) {
toastr.error(data.responseJSON.message)
}
});
});
});
</script>
<style>
.header_actions {
text-align: left !important;
}
.action_group {
margin-right: 20px;
white-space: nowrap;
}
</style>

View File

@ -126,6 +126,7 @@ if ($vars['view'] == 'minigraphs') {
<div style='margin: 0px;'><table class='table'>
<tr>
<th width="350"><A href="<?php echo \LibreNMS\Util\Url::generate($vars, ['sort' => 'port']); ?>">Port</a></th>
<th width="100">Port Group</a></th>
<th width="100"></th>
<th width="120"><a href="<?php echo \LibreNMS\Util\Url::generate($vars, ['sort' => 'traffic']); ?>">Traffic</a></th>
<th width="75">Speed</th>

View File

@ -5,6 +5,7 @@ $(function () {
</script>
<?php
use App\Models\Port;
use LibreNMS\Config;
use LibreNMS\Util\IP;
use LibreNMS\Util\Number;
@ -73,6 +74,10 @@ if ($port_details) {
echo '</span>';
$port_group_name_list = Port::find($port['port_id'])->groups->pluck('name')->toArray() ?: ['Default'];
echo '</td><td width=100>';
echo implode('<br>', $port_group_name_list);
echo "</td><td width=100 onclick=\"location.href='" . generate_port_url($port) . "'\" >";
if ($port_details) {

View File

@ -1,86 +0,0 @@
<?php
$row = 1;
$device_id = $vars['device_id'];
$sql = 'FROM `ports` WHERE `device_id` = ?';
$param = [$device_id];
if (isset($searchPhrase) && ! empty($searchPhrase)) {
$sql .= ' AND (`ifName` LIKE ? OR `ifAlias` LIKE ? OR `ifDescr` LIKE ?)';
$param[] = "%$searchPhrase%";
$param[] = "%$searchPhrase%";
$param[] = "%$searchPhrase%";
}
$count_sql = "SELECT COUNT(`port_id`) $sql";
$total = dbFetchCell($count_sql, $param);
if (empty($total)) {
$total = 0;
}
if (! isset($sort) || empty($sort)) {
$sort = '`ifIndex` ASC';
}
$sql .= " ORDER BY $sort";
if (isset($current)) {
$limit_low = (($current * $rowCount) - ($rowCount));
$limit_high = $rowCount;
}
if ($rowCount != -1) {
$sql .= " LIMIT $limit_low,$limit_high";
}
$sql = "SELECT * $sql";
$response[] = [
'ifIndex' => "<button id='save-form' type='submit' value='Save' class='btn btn-success btn-sm' title='Save current port disable/ignore settings'>Save</button><button type='submit' value='Reset' class='btn btn-danger btn-sm' id='form-reset' title='Reset form to previously-saved settings'>Reset</button>",
'label' => '',
'ifAdminStatus' => '',
'ifOperStatus' => "<button type='submit' value='Alerted' class='btn btn-default btn-sm' id='alerted-toggle' title='Toggle alerting on all currently-alerted ports'>Alerted</button><button type='submit' value='Down' class='btn btn-default btn-sm' id='down-select' title='Disable alerting on all currently-down ports'>Down</button>",
'disabled' => "<button type='submit' value='Toggle' class='btn btn-default btn-sm' id='disable-toggle' title='Toggle polling for all ports'>Toggle</button><button type='submit' value='Select' class='btn btn-default btn-sm' id='disable-select' title='Disable polling on all ports'>Select All</button>",
'ignore' => "<button type='submit' value='Toggle' class='btn btn-default btn-sm' id='ignore-toggle' title='Toggle alert tag for all ports'>Toggle</button><button type='submit' value='Select' class='btn btn-default btn-sm' id='ignore-select' title='Disable alert tag on all ports'>Select All</button>",
'ifAlias' => '',
];
foreach (dbFetchRows($sql, $param) as $port) {
$port = cleanPort($port);
// Mark interfaces which are OperDown (but not AdminDown) yet not ignored or disabled, or up yet ignored or disabled
// - as to draw the attention to a possible problem.
$isportbad = ($port['ifOperStatus'] != 'up' && $port['ifAdminStatus'] != 'down') ? 1 : 0;
$dowecare = ($port['ignore'] == 0 && $port['disabled'] == 0) ? $isportbad : ! $isportbad;
$outofsync = $dowecare ? " class='red'" : '';
$checked = '';
$device['device_id'] = $device_id;
if (get_dev_attrib($device, 'ifName_tune:' . $port['ifName']) == 'true') {
$checked = 'checked';
}
$response[] = [
'ifIndex' => $port['ifIndex'],
'ifName' => $port['label'],
'ifAdminStatus' => $port['ifAdminStatus'],
'ifOperStatus' => '<span name="operstatus_' . $port['port_id'] . '"' . $outofsync . '>' . $port['ifOperStatus'] . '</span>',
'disabled' => '<input type="checkbox" class="disable-check" data-size="small" name="disabled_' . $port['port_id'] . '"' . ($port['disabled'] ? 'checked' : '') . '>
<input type="hidden" name="olddis_' . $port['port_id'] . '" value="' . ($port['disabled'] ? 1 : 0) . '"">',
'ignore' => '<input type="checkbox" class="ignore-check" data-size="small" name="ignore_' . $port['port_id'] . '"' . ($port['ignore'] ? 'checked' : '') . '>
<input type="hidden" name="oldign_' . $port['port_id'] . '" value="' . ($port['ignore'] ? 1 : 0) . '"">',
'port_tune' => '<input type="checkbox" id="override_config" name="override_config" data-attrib="ifName_tune:' . $port['ifName'] . '" data-device_id="' . $port['device_id'] . '" data-size="small" ' . $checked . '>',
'ifAlias' => '<div class="form-group"><input class="form-control input-sm" id="if-alias" name="if-alias" data-device_id="' . $port['device_id'] . '" data-port_id="' . $port['port_id'] . '" data-ifName="' . $port['ifName'] . '" value="' . $port['ifAlias'] . '"><span class="form-control-feedback"><i class="fa" aria-hidden="true"></i></span></div>',
'ifSpeed' => '<div class="form-group has-feedback"><input type="text" pattern="[0-9]*" inputmode="numeric" class="form-control input-sm" id="if-speed" name="if-speed" data-device_id="' . $port['device_id'] . '" data-port_id="' . $port['port_id'] . '" data-ifName="' . $port['ifName'] . '" value="' . $port['ifSpeed'] . '"><span class="form-control-feedback"><i class="fa" aria-hidden="true"></i></span></div>',
];
}//end foreach
$output = [
'current' => $current,
'rowCount' => $rowCount,
'rows' => $response,
'total' => $total,
];
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

View File

@ -1461,6 +1461,25 @@ ports:
PRIMARY: { Name: PRIMARY, Columns: [port_id], Unique: true, Type: BTREE }
ports_device_id_ifindex_unique: { Name: ports_device_id_ifindex_unique, Columns: [device_id, ifIndex], Unique: true, Type: BTREE }
ports_ifdescr_index: { Name: ports_ifdescr_index, Columns: [ifDescr], Unique: false, Type: BTREE }
port_groups:
Columns:
- { Field: id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
- { Field: name, Type: varchar(255), 'Null': false, Extra: ''}
- { Field: desc, Type: varchar(255), 'Null': true, Extra: ''}
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
port_groups_name_unique: { Name: port_groups_name_unique, Columns: [name], Unique: true, Type: BTREE }
port_group_port:
Columns:
- { Field: port_group_id, Type: 'int unsigned', 'Null': false, Extra: '' }
- { Field: port_id, Type: 'int unsigned', 'Null': false, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [port_group_id, port_id], Unique: true, Type: BTREE }
port_group_port_port_group_id_index: { Name: port_group_port_port_group_id_index, Columns: [port_group_id], Unique: false, Type: BTREE }
port_group_port_port_id_index: { Name: port_group_port_port_id_index, Columns: [port_id], Unique: false, Type: BTREE }
Constraints:
port_group_port_port_group_id_foreign: { name: port_group_port_port_group_id_foreign, foreign_key: port_group_id, table: port_groups, key: id, extra: 'ON DELETE CASCADE' }
port_group_port_port_id_foreign: { name: port_group_port_port_id_foreign, foreign_key: port_id, table: ports, key: port_id, extra: 'ON DELETE CASCADE' }
ports_adsl:
Columns:
- { Field: port_id, Type: 'int unsigned', 'Null': false, Extra: '' }

View File

@ -121,6 +121,7 @@ nav:
- DeviceGroups: API/DeviceGroups.md
- Ports: API/Ports.md
- PortGroups: API/PortGroups.md
- Port_Groups: API/Port_Groups.md
- Alerts: API/Alerts.md
- Routing: API/Routing.md
- Switching: API/Switching.md

View File

@ -0,0 +1,8 @@
<?php
return [
'groups' => [
'updated' => ':port: groups updated',
'none' => ':port no update requested',
],
];

View File

@ -335,7 +335,11 @@
@endif
<li role="presentation" class="divider"></li>
<li><a href="{{ url('port-groups') }}"><i class="fa fa-th fa-fw fa-lg"
aria-hidden="true"></i> @lang('Manage Groups')
</a></li>
<li role="presentation" class="divider"></li>
@if($port_counts['alerted'])
<li><a href="{{ url('ports/alerted=yes') }}"><i
class="fa fa-exclamation-circle fa-fw fa-lg"

View File

@ -0,0 +1,26 @@
@extends('layouts.librenmsv1')
@section('title', __('Create Port Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('port-groups.store') }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 col-sm-12">
<legend>@lang('Create Port Group')</legend>
@csrf
@include('port-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('port-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,26 @@
@extends('layouts.librenmsv1')
@section('title', __('Edit Port Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('port-groups.update', $port_group->id) }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-sm-12">
<legend>@lang('Edit Port Group'): {{ $port_group->name }}</legend>
{{ method_field('PUT') }}
@csrf
@include('port-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('port-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
<div class="form-group @if($errors->has('name')) has-error @endif">
<label for="name" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Name')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="name" name="name" value="{{ old('name', $port_group->name) }}">
<span class="help-block">{{ $errors->first('name') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('desc')) has-error @endif">
<label for="desc" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Description')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="desc" name="desc" value="{{ old('desc', $port_group->desc) }}">
<span class="help-block">{{ $errors->first('desc') }}</span>
</div>
</div>

View File

@ -0,0 +1,81 @@
@extends('layouts.librenmsv1')
@section('title', __('Port Groups'))
@section('content')
<div class="container-fluid">
<x-panel id="manage-port-groups-panel">
<x-slot name="title">
<i class="fa fa-th fa-fw fa-lg" aria-hidden="true"></i> @lang('Port Groups')
</x-slot>
<div class="row">
<div class="col-md-12">
<a type="button" class="btn btn-primary" href="{{ route('port-groups.create') }}">
<i class="fa fa-plus"></i> @lang('New Port Group')
</a>
</div>
</div>
<div class="table-responsive">
<table id="manage-port-groups-table" class="table table-condensed table-hover">
<thead>
<tr>
<th>@lang('Name')</th>
<th>@lang('Description')</th>
<th>@lang('Actions')</th>
</tr>
</thead>
<tbody>
@foreach($port_groups as $port_group)
<tr id="row_{{ $port_group->id }}">
<td>{{ $port_group->name }}</td>
<td>{{ $port_group->desc }}</td>
<td>
<a type="button" title="@lang('edit Port Group')" class="btn btn-primary btn-sm" aria-label="@lang('Edit')"
href="{{ route('port-groups.edit', $port_group->id) }}">
<i class="fa fa-pencil" aria-hidden="true"></i></a>
<button type="button" class="btn btn-danger btn-sm" title="@lang('delete Port Group')" aria-label="@lang('Delete')"
onclick="delete_pg(this, '{{ $port_group->name }}', '{{ route('port-groups.destroy', $port_group->id) }}')">
<i
class="fa fa-trash" aria-hidden="true"></i></button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</x-panel>
</div>
@endsection
@section('scripts')
<script>
function delete_pg(button, name, url) {
var index = button.parentNode.parentNode.rowIndex;
if (confirm('@lang('Are you sure you want to delete ')' + name + '?')) {
$.ajax({
url: url,
type: 'DELETE',
success: function (msg) {
document.getElementById("manage-port-groups-table").deleteRow(index);
toastr.success(msg);
},
error: function () {
toastr.error('@lang('The port group could not be deleted')');
}
});
}
return false;
}
</script>
@endsection
@section('css')
<style>
.table-responsive {
padding-top: 16px
}
</style>
@endsection

View File

@ -25,11 +25,21 @@
@endif
</select>
</div>
<div class="form-group">
<label for="port_group-{{ $id }}" class="control-label">@lang('Port group')</label>
<select class="form-control" name="port_group" id="port_group-{{ $id }}" data-placeholder="@lang('All Ports')">
@if($port_group)
<option value="{{ $port_group->id }}" selected>{{ $port_group->name }}</option>
@endif
</select>
</div>
@endsection
@section('javascript')
<script type="text/javascript">
init_select2('#interface_filter-{{ $id }}', 'port-field', {limit: 100, field: 'ifType'}, '{{ $interface_filter ?: '' }}');
init_select2('#device_group-{{ $id }}', 'device-group', {});
init_select2('#port_group-{{ $id }}', 'port-group', {});
</script>
@endsection

View File

@ -23,6 +23,7 @@ Route::group(['prefix' => 'v0', 'namespace' => '\App\Api\Controllers'], function
Route::get('oxidized/{hostname?}', 'LegacyApiController@list_oxidized')->name('list_oxidized');
Route::get('devicegroups/{name}', 'LegacyApiController@get_devices_by_group')->name('get_devices_by_group');
Route::get('devicegroups', 'LegacyApiController@get_device_groups')->name('get_device_groups');
Route::get('port_groups', 'LegacyApiController@get_port_groups')->name('get_port_groups');
Route::get('portgroups/multiport/bits/{id}', 'LegacyApiController@get_graph_by_portgroup')->name('get_graph_by_portgroup_multiport_bits');
Route::get('portgroups/{group}', 'LegacyApiController@get_graph_by_portgroup')->name('get_graph_by_portgroup');
Route::get('alerts/{id}', 'LegacyApiController@list_alerts')->name('get_alert');
@ -74,6 +75,7 @@ Route::group(['prefix' => 'v0', 'namespace' => '\App\Api\Controllers'], function
Route::get('oxidized/config/search/{searchstring}', 'LegacyApiController@search_oxidized')->name('search_oxidized');
Route::get('oxidized/config/{device_name}', 'LegacyApiController@get_oxidized_config')->name('get_oxidized_config');
Route::post('devicegroups', 'LegacyApiController@add_device_group')->name('add_device_group');
Route::post('port_groups', 'LegacyApiController@add_port_group')->name('add_port_group');
Route::post('devices/{id}/parents', 'LegacyApiController@add_parents_to_host')->name('add_parents_to_host');
Route::delete('/devices/{id}/parents', 'LegacyApiController@del_parents_from_host')->name('del_parents_from_host');
Route::post('locations', 'LegacyApiController@add_location')->name('add_location');

View File

@ -19,6 +19,8 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
// pages
Route::resource('device-groups', 'DeviceGroupController');
Route::resource('port-groups', 'PortGroupController');
Route::resource('port', 'PortController', ['only' => 'update']);
Route::group(['prefix' => 'poller'], function () {
Route::get('', 'PollerController@pollerTab')->name('poller.index');
Route::get('log', 'PollerController@logTab')->name('poller.log');
@ -107,6 +109,7 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
Route::get('device', 'DeviceController');
Route::get('device-field', 'DeviceFieldController');
Route::get('device-group', 'DeviceGroupController');
Route::get('port-group', 'PortGroupController');
Route::get('eventlog', 'EventlogController');
Route::get('graph', 'GraphController');
Route::get('graph-aggregate', 'GraphAggregateController');
@ -125,6 +128,7 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
Route::post('alert-schedule', 'AlertScheduleController');
Route::post('customers', 'CustomersController');
Route::post('device', 'DeviceController');
Route::post('edit-ports', 'EditPortsController');
Route::post('eventlog', 'EventlogController');
Route::post('fdb-tables', 'FdbTablesController');
Route::post('graylog', 'GraylogController');