interfaces: revamp overview page (#7019)

This commit omits some of the link-specific information such as ppp uptime, disconnect/release mechanism, as well as wireless and bridge information. Since there is more of this type of information available than was originally being handled by get_interfaces_info(), perhaps it makes more sense to extend the backend script with the relevant bits in time.
This commit is contained in:
Stephan de Wit 2023-11-27 15:52:04 +01:00 committed by GitHub
parent e90571fe6b
commit 5da37a7fac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 758 additions and 168 deletions

4
plist
View File

@ -387,12 +387,14 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LaggSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LoopbackSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/OverviewController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VlanSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VxlanSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/LaggController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/LoopbackController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/OverviewController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VlanController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VxlanController.php
@ -833,6 +835,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/lagg.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/loopback.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/neighbor.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/overview.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/vip.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/vlan.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/vxlan.volt
@ -2152,7 +2155,6 @@
/usr/local/www/vpn_openvpn_client.php
/usr/local/www/vpn_openvpn_server.php
/usr/local/www/widgets/api/get.php
/usr/local/www/widgets/api/plugins/interfaces.inc
/usr/local/www/widgets/api/plugins/system.inc
/usr/local/www/widgets/api/plugins/temperature.inc
/usr/local/www/widgets/include/carp_status.inc

View File

@ -0,0 +1,278 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Interfaces\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Firewall\Util;
use OPNsense\Routing\Gateways;
class OverviewController extends ApiControllerBase
{
private static function translations()
{
return [
'flags' => gettext('Flags'),
'options' => gettext('Options'),
'supported_media' => gettext('Supported Media'),
'is_physical' => gettext('Physical'),
'device' => gettext('Device'),
'name' => gettext('Name'),
'description' => gettext('Description'),
'status' => gettext('Status'),
'enabled' => gettext('Enabled'),
'link_type' => gettext('Link Type'),
'ipv4' => gettext('IPv4 Addresses'),
'ipv6' => gettext('IPv6 Addresses'),
'gateways' => gettext('Gateways'),
'routes' => gettext('Routes'),
'macaddr' => gettext('MAC Address'),
'media_raw' => gettext('Media'),
'mediaopt' => gettext('Media Options'),
'capabilities' => gettext('Capabilities'),
'identifier' => gettext('Identifier'),
'ipaddr' => gettext('IP Address'),
'subnetbits' => gettext('Subnet Bits'),
'statistics' => gettext('Statistics'),
'driver' => gettext('Driver'),
'index' => gettext('Index'),
'promiscuous listeners' => gettext('Promiscuous Listeners'),
'send queue length' => gettext('Send Queue Length'),
'send queue max length' => gettext('Send Queue Max Length'),
'send queue drops' => gettext('Send Queue Drops'),
'type' => gettext('Type'),
'address length' => gettext('Address Length'),
'header length' => gettext('Header Length'),
'link state' => gettext('Link State'),
'datalen' => gettext('Data Length'),
'metric' => gettext('Metric'),
'line rate' => gettext('Line Rate'),
'packets received' => gettext('Packets Received'),
'input errors' => gettext('Input Errors'),
'packets transmitted' => gettext('Packets Transmitted'),
'output errors' => gettext('Output Errors'),
'collisions' => gettext('Collisions'),
'bytes received' => gettext('Bytes Received'),
'bytes transmitted' => gettext('Bytes Transmitted'),
'multicasts received' => gettext('Multicasts Received'),
'multicasts transmitted' => gettext('Multicasts Transmitted'),
'input queue drops' => gettext('Input Queue Drops'),
'packets for unknown protocol' => gettext('Packets for Unknown Protocol'),
'HW offload capabilities' => gettext('Hardware Offload Capabilities'),
'uptime at attach or stat reset' => gettext('Uptime at Attach or Statistics Reset'),
];
}
private function parseIfInfo($interface = null, $detailed = false)
{
$backend = new Backend();
$gateways = new Gateways();
$cfg = Config::getInstance()->object();
$result = [];
/* quick information */
$ifinfo = json_decode($backend->configdpRun('interface list ifconfig', [$interface]), true);
$routes = json_decode($backend->configdRun('interface routes list -n json'), true);
/* detailed information */
if ($detailed) {
$stats = json_decode($backend->configdpRun('interface list stats', [$interface]), true);
if (!$interface) {
foreach ($ifinfo as $if => $info) {
if (array_key_exists($if, $stats)) {
$ifinfo[$if]['statistics'] = $stats[$if];
}
}
} else {
$ifinfo[$interface]['statistics'] = $stats;
}
}
/* map routes to interfaces */
foreach($routes as $route) {
if (!empty($route['netif']) && !empty($ifinfo[$route['netif']])) {
$ifinfo[$route['netif']]['routes'][] = $route['destination'];
}
}
/* combine interfaces details with config */
foreach ($cfg->interfaces->children() as $key => $node) {
if (!empty((string)$node->if) && !empty($ifinfo[(string)$node->if])) {
$props = [];
foreach ($node->children() as $property) {
$props[$property->getName()] = (string)$property;
}
$ifinfo[(string)$node->if]['config'] = $props;
$ifinfo[(string)$node->if]['config']['identifier'] = $key;
}
}
/* format information */
foreach ($ifinfo as $if => $details) {
$tmp = $details;
if ($if == 'pfsync0') {
continue;
}
$tmp['status'] = (!empty($details['flags']) && in_array('up', $details['flags'])) ? 'up' : 'down';
if (!empty($details['status'])) {
if (!(in_array($details['status'] , ['active', 'running']))) {
/* reflect current ifconfig status, such as 'no carrier' */
$tmp['status'] = $details['status'];
}
}
if (empty($details['config'])) {
$tmp['identifier'] = '';
$tmp['description'] = gettext('Unassigned Interface');
$result[] = $tmp;
continue;
}
$config = $details['config'];
$tmp['identifier'] = $config['identifier'];
$tmp['description'] = !empty($config['descr']) ? $config['descr'] : strtoupper($config['identifier']);
$tmp['enabled'] = !empty($config['enable']);
$tmp['link_type'] = !empty($config['ipaddr']) ? $config['ipaddr'] : 'none';
if (Util::isIpAddress($tmp['link_type'])) {
$tmp['link_type'] = 'static';
}
/* parse IP configuration */
unset($tmp['ipv4'], $tmp['ipv6']);
foreach (['ipv4', 'ipv6'] as $ipproto) {
if (!empty($details[$ipproto])) {
foreach ($details[$ipproto] as $ip) {
if (!empty($ip['ipaddr'])) {
$entry = [];
$entry['ipaddr'] = $ip['ipaddr'] . '/' . $ip['subnetbits'];
if (!empty($ip['vhid'])) {
$vhid = $ip['vhid'];
$entry['vhid'] = $vhid;
if (!empty($details['carp'])) {
foreach ($details['carp'] as $carp) {
if ($carp['vhid'] == $vhid) {
$entry['status'] = $carp['status'];
$entry['advbase'] = $carp['advbase'];
$entry['advskew'] = $carp['advskew'];
}
}
}
}
$tmp[$ipproto][] = $entry;
}
}
}
}
/* gateway(s) */
$gatewayv4 = $gateways->getInterfaceGateway($tmp['identifier'] , 'inet');
$gatewayv6 = $gateways->getInterfaceGateway($tmp['identifier'], 'inet6');
$tmp['gateways'] = array_filter([$gatewayv4, $gatewayv6]);
$result[] = $tmp;
}
return $result;
}
public function interfacesInfoAction($details = false)
{
$this->sessionClose();
$result = $this->parseIfInfo(null, $details);
return $this->searchRecordsetBase($result);
}
public function getInterfaceAction($if = null)
{
$this->sessionClose();
$result = ["message" => "failed"];
if ($if != null) {
$ifinfo = $this->parseIfInfo($if, true)[0] ?? [];
if (!empty($ifinfo)) {
if (!empty($ifinfo['macaddr'])) {
$macs = json_decode((new Backend())->configdRun('interface list macdb json'), true);
$mac_hi = strtoupper(substr(str_replace(':', '', $ifinfo['macaddr']), 0, 6));
if (array_key_exists($mac_hi, $macs)) {
$ifinfo['macaddr'] = $ifinfo['macaddr'] . ' - ' . $macs[$mac_hi];
}
}
/* move statistics one level up */
if (isset($ifinfo['statistics'])) {
$stats = $ifinfo['statistics'];
unset($ifinfo['statistics']);
$ifinfo = array_merge($ifinfo, $stats);
}
unset($ifinfo['config']);
/* apply translations */
foreach ($ifinfo as $key => $value) {
$ifinfo[$key] = [
'value' => $value,
'translation' => self::translations()[$key] ?? $key
];
}
$result['message'] = $ifinfo;
}
}
return json_encode($result);
}
public function reloadInterfaceAction($identifier = null)
{
$this->sessionClose();
$result = ["message" => "failed"];
if ($identifier != null) {
$backend = new Backend();
$result['message'] = $backend->configdpRun('interface reconfigure', [$identifier]);
}
return $result;
}
public function exportAction()
{
$this->sessionClose();
$this->response->setRawHeader('Content-Type: application/json');
$this->response->setRawHeader('Content-Disposition: attachment; filename=ifconfig.json');
echo (new Backend())->configdRun('interface list ifconfig');
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* Copyright (C) 2013 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Interfaces;
class OverviewController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/Interface/overview');
}
}

View File

@ -99,6 +99,7 @@
<Interfaces order="30" cssClass="fa fa-sitemap">
<Assignments order="900" url="/interfaces_assign.php" cssClass="fa fa-pencil fa-fw"/>
<Overview order="910" url="/status_interfaces.php" cssClass="fa fa-tasks fa-fw"/>
<Overview_new VisibleName="Overview [new]" order="915" url="/ui/interfaces/overview" cssClass="fa fa-tasks fa-fw"/>
<Settings order="920" url="/system_advanced_network.php" cssClass="fa fa-cogs fa-fw"/>
<Neighbors order="930" url="/ui/interfaces/neighbor" cssClass="fa fa-users fa-fw"/>
<Wireless order="940" cssClass="fa fa-wifi fa-fw">

View File

@ -0,0 +1,318 @@
{#
# Copyright (c) 2023 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$( document ).ready(function() {
function format_linerate(value) {
if (!isNaN(value) && value > 0) {
let fileSizeTypes = ["", "K", "M", "G", "T", "P", "E", "Z", "Y"];
let ndx = Math.floor(Math.log(value) / Math.log(1000) );
if (ndx > 0) {
return (value / Math.pow(1000, ndx)).toFixed(2) + ' ' + fileSizeTypes[ndx] + 'bit/s';
} else {
return value.toFixed(2).toString();
}
} else {
return "";
}
}
function iterate_ips(obj) {
let $elements = $('<div></div>');
obj.forEach(function (ip) {
$span = $('<span></span><br/>').text(ip['ipaddr'] + ' ');
if ('vhid' in ip) {
$carp = $('<span></span>').text('vhid ' + ip['vhid']);
$carp.attr('class', 'bootgrid-tooltip badge badge-pill');
$carp.css('background-color', ip['status'] == 'MASTER' ? 'green' : 'primary');
$carp.attr('data-toggle', 'tooltip');
$carp.attr('title', ip['status']);
$span.append($carp);
}
$elements.append($span);
});
return $elements.prop('outerHTML');
}
$("#grid-overview").UIBootgrid(
{
search: '/api/interfaces/overview/interfacesInfo',
options: {
selection: false,
formatters: {
"interface": function (column, row) {
let descr = row.description;
if (row.identifier) {
descr += ' (' + row.identifier + ')';
}
return descr;
},
"routes": function (column, row) {
let $elements = $('<div></div>').attr('class', 'route-container');
if (row.routes) {
let i = 0;
row.routes.forEach(function (route) {
let $route = $('<span></span>').attr('class', 'route-content').text(route);
if (route == 'default') {
$route.css('color', 'green');
}
if (i > 1) {
$route.css("display", "none");
}
$elements.append($route.append($('<br/>')));
i++;
});
$elements.append($('<button></button>')
.attr('class', 'route-expand btn btn-primary btn-xs')
.text('Expand'));
}
return $elements.prop('outerHTML');
},
"status": function (column, row) {
let connected = row.status == 'up' ? 'text-success' : 'text-danger';
if (!row.enabled) {
row.status += ' (disabled)';
}
return '<i class="fa fa-plug ' + connected + '" title="' + row.status + '" data-toggle="tooltip"></i>';
},
"ipv4": function (column, row) {
if (row.ipv4) {
return iterate_ips(row.ipv4);
}
return '';
},
"ipv6": function (column, row) {
if (row.ipv6) {
return iterate_ips(row.ipv6);
}
return '';
},
"gateways": function (column, row) {
let $elements = $('<div></div>');
if (row.gateways) {
row.gateways.forEach(function (gw) {
let $span = $('<span></span><br/>').text(gw);
$elements.append($span);
});
}
return $elements.prop('outerHTML');
},
"commands": function (column, row) {
let $commands = $('<div></div>');
let $btn = $('<button type="button" class="btn btn-xs btn-default" data-toggle="tooltip"">\
<i></i></button>');
if ('link_type' in row) {
if (["dhcp", "pppoe", "pptp", "l2tp", "ppp"].includes(row.link_type)) {
let $command = $btn.clone();
$command.addClass('interface-reload').attr('title', 'Reload').attr('data-device-id', row.identifier);
$command.find('i').addClass('fa fa-fw fa-refresh');
$commands.append($command);
}
}
$btn.addClass('interface-info').attr('title', 'Info').attr('data-row-id', row.device);
$btn.find('i').addClass('fa fa-fw fa-search');
$commands.append($btn);
return $commands.prop('outerHTML');
}
}
}
}
).on("loaded.rs.jquery.bootgrid", function (e) {
$('[data-toggle="tooltip"]').tooltip();
/* attach event handler to reload buttons */
$('.interface-reload').each(function () {
$(this).click(function () {
let $element = $(this);
let device = $(this).data("device-id");
$element.remove('i').html('<i class="fa fa-spinner fa-spin"></i>');
ajaxCall('/api/interfaces/overview/reloadInterface/' + device, {}, function (data, status) {
/* delay slightly to allow the interface to come up */
setTimeout(function() {
$element.remove('i').html('<i class="fa fa-fw fa-refresh"></i>');
$("#grid-overview").bootgrid('reload');
}, 1000);
});
});
});
/* attach event handler to the command-info button */
$(".interface-info").each(function () {
$(this).click(function () {
let $element = $(this);
let device = $(this).data("row-id");
ajaxGet('/api/interfaces/overview/getInterface/' + device, {}, function(data, status) {
data = data['message'];
let $table = $('<table class="table table-bordered table-condensed table-hover table-striped"></table>');
let $table_body = $('<tbody/>');
for (let key in data) {
let $row = $('<tr/>');
let value = data[key]['value'];
if (key === 'line rate') {
value = format_linerate(value.split(" ")[0]);
}
if (key === 'ipv4' || key === 'ipv6') {
value = iterate_ips(value);
}
if (!'translation' in data[key]) {
continue;
}
key = data[key]['translation'];
if (typeof value === 'string' || Array.isArray(value)) {
value = value.toString().split(",").join("<br/>");
}
$row.append($('<td/>').text(key));
$row.append($('<td/>').html(value));
$table_body.append($row);
}
$table.append($table_body);
$('[data-toggle="tooltip"]').tooltip();
BootstrapDialog.show({
title: data['description']['value'],
message: $table.prop('outerHTML'),
type: BootstrapDialog.TYPE_INFO,
draggable: true,
cssClass: 'details-dialog',
buttons: [{
label: "{{ lang._('Close') }}",
action: function (dialogRef) {
dialogRef.close();
}
}]
});
});
});
});
$(".route-container").each(function () {
let $route_container = $(this);
let count = $(this).children('.route-content').length;
let $expand = $(this).find(".route-expand");
if (count > 2) {
$expand.show();
}
$expand.click(function () {
let $collapsed = $route_container.children('.route-content').filter(function() {
return $(this).css('display').toLowerCase().indexOf('none') > -1;
});
if ($collapsed.length > 0) {
$collapsed.show();
$expand.html('Collapse');
} else {
$collapse = $route_container.children('.route-content').slice(2);
$collapse.hide();
$expand.html('Expand');
}
});
});
});
$("#export-wrapper").detach().appendTo('#grid-overview-header > .row > .actionBar > .btn-group');
$("#export").click(function () {
$('<a></a>').attr('href', '/api/interfaces/overview/export').get(0).click();
});
});
</script>
<style>
.route-content {
white-space: pre-line;
text-overflow: ellipsis;
overflow: hidden;
}
.route-expand {
display: none;
}
.bootgrid-table {
table-layout: auto;
}
.bootgrid-table td {
text-align: center;
vertical-align: middle;
}
.bootgrid-table th {
text-align: center;
vertical-align: middle;
}
.details-dialog .modal-dialog{
position: relative;
display: table;
overflow-y: auto;
overflow-x: auto;
width: auto;
min-width: 600px;
}
.details-dialog .modal-body {
height: 60vh;
overflow-y: auto;
}
</style>
<div class="tab-content content-box">
<div id="export-wrapper" class="btn-group">
<button id="export" class="btn btn-default" data-toggle="tooltip" title="" type="button" data-original-title="Download raw ifconfig output">
<span class="fa fa-cloud-download"></span>
</button>
</div>
<table id="grid-overview" class="table table-bordered table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="status" data-width="5em" data-formatter="status" data-type="string">{{ lang._('Status') }}</th>
<th data-column-id="description" data-formatter="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="device" data-identifier="true" data-width="5em" data-type="string">{{ lang._('Device') }}</th>
<th data-column-id="link_type" data-type="string">{{ lang._('Link Type') }}</th>
<th data-column-id="ipv4" data-formatter="ipv4" data-type="string">{{ lang._('IPv4') }}</th>
<th data-column-id="ipv6" data-formatter="ipv6" data-type="string">{{ lang._('IPv6') }}</th>
<th data-column-id="gateways" data-formatter="gateways" data-type="string">{{ lang._('Gateway') }}</th>
<th data-column-id="routes" data-formatter="routes" data-type="string">{{ lang._('Routes') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>
</div>

View File

@ -91,10 +91,16 @@ message:request mac table
[list.ifconfig]
command:/usr/local/sbin/pluginctl -D
parameters:
parameters: %s
type:script_output
message:request ifconfig
[list.stats]
command:/usr/local/sbin/pluginctl -I
parameters: %s
type:script_output
message:request interface stats
[show.traffic]
command:/usr/local/opnsense/scripts/interfaces/traffic_stats.php
parameters:

View File

@ -1,67 +0,0 @@
<?php
/*
Copyright (C) 2016 Deciso B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
/**
* widget interfaces data
*/
function interfaces_api()
{
$result = array();
$ifsinfo = get_interfaces_info();
foreach (get_configured_interface_with_descr() as $ifdescr => $ifname) {
$ifinfo = $ifsinfo[$ifdescr];
$interfaceItem = array();
$interfaceItem['inpkts'] = $ifinfo['inpkts'];
$interfaceItem['outpkts'] = $ifinfo['outpkts'];
$interfaceItem['inbytes'] = $ifinfo['inbytes'];
$interfaceItem['outbytes'] = $ifinfo['outbytes'];
$interfaceItem['inbytes_frmt'] = format_bytes($ifinfo['inbytes']);
$interfaceItem['outbytes_frmt'] = format_bytes($ifinfo['outbytes']);
$interfaceItem['inerrs'] = isset($ifinfo['inerrs']) ? $ifinfo['inerrs'] : "0";
$interfaceItem['outerrs'] = isset($ifinfo['outerrs']) ? $ifinfo['outerrs'] : "0";
$interfaceItem['collisions'] = isset($ifinfo['collisions']) ? $ifinfo['collisions'] : "0";
$interfaceItem['descr'] = $ifdescr;
$interfaceItem['name'] = $ifname;
switch ($ifinfo['status']) {
case 'down':
case 'no carrier':
case 'up':
$interfaceItem['status'] = $ifinfo['status'];
break;
case 'associated':
$interfaceItem['status'] = 'up';
break;
default:
$interfaceItem['status'] = '';
break;
}
$interfaceItem['ipaddr'] = empty($ifinfo['ipaddr']) ? "" : $ifinfo['ipaddr'];
$interfaceItem['media'] = empty($ifinfo['media']) ? "" : $ifinfo['media'];
$result[] = $interfaceItem;
}
return $result;
}

View File

@ -1,4 +1,4 @@
<?php
$interface_list_title = gettext('Interfaces');
$interface_list_title_link = 'status_interfaces.php';
$interface_list_title_link = 'interfaces_assign.php';

View File

@ -1,4 +1,4 @@
<?php
$interface_statistics_title = gettext('Interface Statistics');
$interface_statistics_title_link = 'status_interfaces.php';
$interface_statistics_title_link = 'ui/interfaces/overview';

View File

@ -61,34 +61,67 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
?>
<script>
/**
* Hybrid widget only update interface status using ajax
*/
function interface_widget_update(sender, data)
{
data.map(function(interface_data) {
var tr_id = 'interface_widget_item_' + interface_data['name'];
if ($("#"+tr_id).length) {
switch (interface_data['status']) {
case 'up':
$("#"+tr_id).find('.text-danger').removeClass('text-danger').addClass('text-success');
$("#"+tr_id).find('.fa-arrow-down').removeClass('fa-arrow-down').addClass('fa-arrow-up');
$("#"+tr_id).find('.fa-times').removeClass('fa-times').addClass('fa-arrow-up');
break;
case 'down':
$("#"+tr_id).find('.text-success').removeClass('text-success').addClass('text-danger');
$("#"+tr_id).find('.fa-arrow-up').removeClass('fa-arrow-up').addClass('fa-arrow-down');
$("#"+tr_id).find('.fa-times').removeClass('fa-times').addClass('fa-arrow-down');
break;
default:
$("#"+tr_id).find('.text-success').removeClass('text-success').addClass('text-danger');
$("#"+tr_id).find('.fa-arrow-down').removeClass('fa-arrow-down').addClass('fa-times');
$("#"+tr_id).find('.fa-arrow-up').removeClass('fa-arrow-up').addClass('fa-times');
break;
}
}
});
}
ajaxGet('/api/interfaces/overview/interfacesInfo', {}, function(data, status) {
data.rows.map(function(interface_data) {
var tr_id = 'interface_widget_item_' + interface_data['identifier'];
if ($("#"+tr_id).length) {
let row = $("#"+tr_id);
let $link_status = $("#"+tr_id).find('#link_status');
let $if_status = $("#"+tr_id).find('#if_status');
$link_status.empty();
$if_status.empty();
let $status_symbol = $('<span></span>');
switch (interface_data['link_type']) {
case 'ppp':
$status_symbol.addClass('fa fa-mobile');
break;
case 'wireless':
$status_symbol.addClass('fa fa-signal');
break;
default:
$status_symbol.addClass('fa fa-exchange');
break;
}
$link_status.append($status_symbol.addClass('text-success'));
$if_status.append($('<span class="fa fa-arrow-up text-success" title="' + interface_data['status'] + '"></span>'));
switch (interface_data['status']) {
case 'up':
//case 'associated':
$("#"+tr_id).find('.text-danger').removeClass('text-danger').addClass('text-success');
$("#"+tr_id).find('.fa-arrow-down').removeClass('fa-arrow-down').addClass('fa-arrow-up');
$("#"+tr_id).find('.fa-times').removeClass('fa-times').addClass('fa-arrow-up');
break;
case 'down':
$("#"+tr_id).find('.text-success').removeClass('text-success').addClass('text-danger');
$("#"+tr_id).find('.fa-arrow-up').removeClass('fa-arrow-up').addClass('fa-arrow-down');
$("#"+tr_id).find('.fa-times').removeClass('fa-times').addClass('fa-arrow-down');
break;
default:
$("#"+tr_id).find('.text-success').removeClass('text-success').addClass('text-danger');
$("#"+tr_id).find('.fa-arrow-down').removeClass('fa-arrow-down').addClass('fa-times');
$("#"+tr_id).find('.fa-arrow-up').removeClass('fa-arrow-up').addClass('fa-times');
break;
}
let $name = $("#"+tr_id).find('#if_name');
$name.empty();
let $name_span = $('<span style="cursor:pointer" onclick="location.href=\'/interfaces.php?if=' + interface_data['identifier'] +
'\'"><strong><u>' + interface_data['description'] +'</strong></u></span>');
$name.append($name_span)
$media = $("#"+tr_id).find('#if_media');
$media.empty();
if (!'media' in interface_data) {
$media.html(interface_data['cell_mode']);
} else {
$media.html(interface_data['media']);
}
}
});
});
</script>
<div id="interface_list-settings" class="widgetconfigdiv" style="display:none;">
@ -112,61 +145,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
</form>
</div>
<table class="table table-striped table-condensed" data-plugin="interfaces" data-callback="interface_widget_update">
<table class="table table-striped table-condensed">
<?php
$ifsinfo = get_interfaces_info();
foreach ($interfaces as $ifdescr => $ifname):
$listed = in_array($ifdescr, $pconfig['interfaceslistfilter']);
foreach ($interfaces as $ident => $ifname):
$listed = in_array($ident, $pconfig['interfaceslistfilter']);
$listed = !empty($pconfig['interfaceslistinvert']) ? $listed : !$listed;
if (!$listed) {
continue;
}
$ifinfo = $ifsinfo[$ifdescr]; ?>
<tr id="interface_widget_item_<?= html_safe($ifname) ?>">
<td style="width:5%; word-break: break-word;">
<?php if (isset($ifinfo['ppplink'])): ?>
<span title="3g" class="fa fa-mobile text-success"></span>
<?php elseif (isset($config['interfaces'][$ifdescr]['wireless'])): ?>
<?php if ($ifinfo['status'] == 'associated' || $ifinfo['status'] == 'up'): ?>
<span title="wlan" class="fa fa-signal text-success"></span>
<?php else: ?>
<span title="wlan_d" class="fa fa-signal text-danger"></span>
<?php endif ?>
<?php else: ?>
<?php if ($ifinfo['status'] == 'up'): ?>
<span title="cablenic" class="fa fa-exchange text-success"></span>
<?php else: ?>
<span title="cablenic" class="fa fa-exchange text-danger"></span>
<?php endif ?>
<?php endif ?>
</td>
<td style="width:15%; word-break: break-word;">
<strong>
<u>
<span onclick="location.href='/interfaces.php?if=<?= html_safe($ifdescr) ?>'" style="cursor:pointer">
<?= html_safe($ifname) ?>
</span>
</u>
</strong>
</td>
<td style="width:5%; word-break: break-word;">
<?php if ($ifinfo['status'] == 'up' || $ifinfo['status'] == 'associated'): ?>
<span class="fa fa-arrow-up text-success"></span>
<?php elseif ($ifinfo['status'] == "down"): ?>
<span class="fa fa-arrow-down text-danger"></span>
<?php elseif ($ifinfo['status'] == "no carrier"): ?>
<span class="fa fa-times text-danger"></span>
<?php else: ?>
<?= html_safe($ifinfo['status']) ?>
<?php endif ?>
</td>
<td style="width:32%; word-break: break-word;">
<?= html_safe(empty($ifinfo['media']) ? $ifinfo['cell_mode'] ?? '' : $ifinfo['media']) ?>
</td>
list($primary4,, $bits4) = interfaces_primary_address($ident);
list($primary6,, $bits6) = interfaces_primary_address6($ident);
?>
<tr id="interface_widget_item_<?= html_safe($ident) ?>">
<td style="width:5%; word-break: break-word;" id="link_status"></td>
<td style="width:15%; word-break: break-word;" id="if_name"></td>
<td style="width:5%; word-break: break-word;" id="if_status"></td>
<td style="width:32%; word-break: break-word;" id="if_media"></td>
<td style="width:43%; word-break: break-word;">
<?= html_safe($ifinfo['ipaddr']) ?>
<?= !empty($ifinfo['ipaddr']) ? '<br/>' : '' ?>
<?= html_safe(interfaces_has_prefix_only($ifdescr) ? $ifinfo['linklocal'] : $ifinfo['ipaddrv6']) ?>
<?= html_safe($primary4) ?>
<?= !empty($primary4) ? '<br/>' : '' ?>
<?= html_safe($primary6) ?>
</td>
</tr>
<?php endforeach ?>

View File

@ -71,25 +71,41 @@ $ifvalues = array(
?>
<script>
/**
* update interface statistics
*/
function interface_statistics_widget_update(sender, data)
{
data.map(function(interface_data) {
// fill in stats, use column index to determine td location
var item_index = $("#interface_statistics_widget_intf_" + interface_data['descr']).index();
if (item_index != -1) {
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(1)").html(parseInt(interface_data['inpkts']).toLocaleString());
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(2)").html(parseInt(interface_data['outpkts']).toLocaleString());
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(3)").html(interface_data['inbytes_frmt']);
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(4)").html(interface_data['outbytes_frmt']);
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(5)").html(interface_data['inerrs']);
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(6)").html(interface_data['outerrs']);
$("#interface_statistics_widget_intf_" + interface_data['descr'] +" > td:eq(7)").html(interface_data['collisions']);
}
});
}
function format_field(value) {
if (!isNaN(value) && value > 0) {
let fileSizeTypes = ["", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let ndx = Math.floor(Math.log(value) / Math.log(1000) );
if (ndx > 0) {
return (value / Math.pow(1000, ndx)).toFixed(2) + ' ' + fileSizeTypes[ndx];
} else {
return value.toFixed(2);
}
} else {
return "0";
}
}
/**
* update interface statistics
*/
ajaxGet('/api/interfaces/overview/interfacesInfo/1', {}, function(data, status) {
data.rows.map(function(interface_data) {
let stats = interface_data['statistics'];
let inbytes = format_field(parseInt(stats['bytes received']));
let outbytes = format_field(parseInt(stats['bytes transmitted']));
// fill in stats, use column index to determine td location
var item_index = $("#interface_statistics_widget_intf_" + interface_data['identifier']).index();
if (item_index != -1) {
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(1)").html(parseInt(stats['packets received']).toLocaleString());
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(2)").html(parseInt(stats['packets transmitted']).toLocaleString());
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(3)").html(inbytes);
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(4)").html(outbytes);
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(5)").html(stats['input errors']);
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(6)").html(stats['output errors']);
$("#interface_statistics_widget_intf_" + interface_data['identifier'] +" > td:eq(7)").html(stats['collisions']);
}
});
});
</script>
<div id="interface_statistics-settings" class="widgetconfigdiv" style="display:none;">
@ -113,7 +129,7 @@ $ifvalues = array(
</form>
</div>
<table class="table table-striped table-condensed" data-plugin="interfaces" data-callback="interface_statistics_widget_update">
<table class="table table-striped table-condensed">
<tr>
<th>&nbsp;</th>
<?php foreach ($ifvalues as $ifkey => $iflabel): ?>