net/frr: Diagnostics overhaul (#3484)

* net/frr: Diagnostics overhaul [2] - https://github.com/opnsense/plugins/pull/3263
o new extensible template for tabbed views
o get rid of diagnostics form - was unused anyway
o get rid of lodash dependency
o the API output is HTML-encoded, this fixes resulting display issues
o move search functionality to controller.
o move bgp additional attributes inside the record set so people can show them on request (the tree functions might better to move into a core Javascript file to reduce duplication, but for now this is fine.)
o add formatters in bootgrid to mimic the previous behavior
o add interface names (descriptions) for easier reading in route endpoints
o cleanups, move bgp routerid and localAS to subtitle and offer a "generic" hook for endpoints. remove unused elements from the volt template.

---------

Co-authored-by: Marc Leuser <github@mleuser.de>
This commit is contained in:
Ad Schellevis
2023-06-26 15:55:00 +02:00
committed by GitHub
co-authored by Marc Leuser
parent 25a1684452
commit 39bbffb928
9 changed files with 801 additions and 776 deletions
@@ -41,11 +41,27 @@ use OPNsense\Core\Config;
*/
class DiagnosticsController extends ApiControllerBase
{
private $allifnames = [];
public function initialize()
{
parent::initialize();
foreach (Config::getInstance()->object()->interfaces->children() as $key => $node) {
$this->allifnames[(string)$node->if] = !empty((string)$node->descr) ? (string)$node->descr : $key;
}
}
public function getIfDesc($ifname)
{
return !empty($this->allifnames[$ifname]) ? $this->allifnames[$ifname] : '';
}
private function getInformation(string $daemon, string $name, string $format): array
{
$backend = new Backend();
$response = $backend->configdRun("quagga diagnostics " . $daemon . "_" . $name . ($format === "json" ? "_json" : ""));
return array("response" => ($format === "json" ? json_decode($response) : $response));
$response = (new Backend())->configdRun(
"quagga diagnostics " . $daemon . "_" . $name . ($format === "json" ? "_json" : "")
);
return ["response" => ($format === "json" ? json_decode($response ?? '', true) : $response)];
}
public function generalrunningconfigAction(): array
@@ -53,6 +69,9 @@ class DiagnosticsController extends ApiControllerBase
return $this->getInformation("general", "running-config", "plain");
}
/**
* XXX: unused, deprecate in next major?
*/
public function generalrouteAction($format = "json"): array
{
$routes4 = $this->getInformation("general", "route4", $format)['response'];
@@ -69,11 +88,45 @@ class DiagnosticsController extends ApiControllerBase
return $this->getInformation("general", "route4", $format);
}
public function searchGeneralroute4Action(): array
{
$records = [];
foreach($this->getInformation("general", "route4", "json")['response'] as $routes) {
foreach ($routes as $route) {
foreach ($route['nexthops'] as $nexthop) {
$nexthop = array_merge($route, $nexthop);
unset($nexthop['nexthops']);
$nexthop['via'] = !empty($nexthop['ip']) ? $nexthop['ip'] : 'Directly Attached';
$nexthop['interfaceDescr'] = $this->getIfDesc($nexthop['interfaceName'] ?? '');
$records[] = $nexthop;
}
}
}
return $this->searchRecordsetBase($records);
}
public function generalroute6Action($format = "json"): array
{
return $this->getInformation("general", "route6", $format);
}
public function searchGeneralroute6Action(): array
{
$records = [];
foreach($this->getInformation("general", "route6", "json")['response'] as $routes) {
foreach ($routes as $route) {
foreach ($route['nexthops'] as $nexthop) {
$nexthop = array_merge($route, $nexthop);
unset($nexthop['nexthops']);
$nexthop['via'] = !empty($nexthop['ip']) ? $nexthop['ip'] : 'Directly Attached';
$nexthop['interfaceDescr'] = $this->getIfDesc($nexthop['interfaceName'] ?? '');
$records[] = $nexthop;
}
}
}
return $this->searchRecordsetBase($records);
}
public function bgprouteAction($format = "json"): array
{
return $this->getInformation("bgp", "route", $format);
@@ -84,11 +137,83 @@ class DiagnosticsController extends ApiControllerBase
return $this->getInformation("bgp", "route4", $format);
}
public function searchBgproute4Action(): array
{
$records = [];
$payload = $this->getInformation("bgp", "route4", "json")['response'];
$baserecord = [];
foreach ($payload as $key => $value) {
if (!is_array($value)) {
$baserecord[$key] = $value;
}
}
if (!empty($payload['routes'])) {
foreach ($payload['routes'] as $routes) {
foreach ($routes as $route) {
foreach ($route['nexthops'] as $nexthop) {
$nexthop = array_merge($route, $nexthop);
unset($nexthop['nexthops']);
$nexthop['internal'] = !empty($nexthop['pathFrom']) && $nexthop['pathFrom'] == 'internal';
$nexthop['path'] = !empty($nexthop['path']) ? $nexthop['path'] : 'Internal';
$records[] = array_merge($baserecord, $nexthop);
}
}
}
}
$result = $this->searchRecordsetBase($records);
if (!empty($baserecord)) {
$result['subtitle'] = sprintf(
'%s : %s , %s : %s',
gettext('routerId'),
$baserecord['routerId'],
gettext('localAS'),
$baserecord['localAS']
);
}
return $result;
}
public function bgproute6Action($format = "json"): array
{
return $this->getInformation("bgp", "route6", $format);
}
public function searchBgproute6Action(): array
{
$records = [];
$payload = $this->getInformation("bgp", "route6", "json")['response'];
$baserecord = [];
foreach ($payload as $key => $value) {
if (!is_array($value)) {
$baserecord[$key] = $value;
}
}
if (!empty($payload['routes'])) {
foreach ($payload['routes'] as $routes) {
foreach ($routes as $route) {
foreach ($route['nexthops'] as $nexthop) {
$nexthop = array_merge($route, $nexthop);
unset($nexthop['nexthops']);
$nexthop['internal'] = !empty($nexthop['pathFrom']) && $nexthop['pathFrom'] == 'internal';
$nexthop['path'] = !empty($nexthop['path']) ? $nexthop['path'] : 'Internal';
$records[] = array_merge($baserecord, $nexthop);
}
}
}
}
$result = $this->searchRecordsetBase($records);
if (!empty($baserecord)) {
$result['subtitle'] = sprintf(
'%s : %s , %s : %s',
gettext('routerId'),
$baserecord['routerId'],
gettext('localAS'),
$baserecord['localAS']
);
}
return $result;
}
public function bgpsummaryAction($format = "json"): array
{
return $this->getInformation("bgp", "summary", $format);
@@ -109,11 +234,50 @@ class DiagnosticsController extends ApiControllerBase
return $this->getInformation("ospf", "neighbor", $format);
}
public function searchOspfneighborAction(): array
{
$records = [];
$payload = $this->getInformation("ospf", "neighbor", "json")['response'];
if (!empty($payload['neighbors'])) {
foreach ($payload['neighbors'] as $neighborid => $neighbor) {
foreach ($neighbor as $item) {
$item['neighborid'] = $neighborid;
$records[] = $item;
}
}
}
return $this->searchRecordsetBase($records);
}
public function ospfrouteAction($format = "json"): array
{
return $this->getInformation("ospf", "route", $format);
}
public function searchOspfrouteAction(): array
{
$records = [];
$payload = $this->getInformation("ospf", "route", "json")['response'];
foreach($payload as $net => $network) {
if (empty($network['nexthops'])) {
continue;
}
foreach ($network['nexthops'] as $nexthop) {
$records[] = [
'type' => $network['routeType'],
'network' => $net,
'cost' => $network['cost'],
'area' => $network['area'] ?? '',
'via' => !empty($nexthop['via']) ? $nexthop['ip'] : 'Directly Attached',
'viainterface' => !empty($nexthop['via']) ? $nexthop['via'] : $nexthop['directly attached to'],
'viainterfaceDescr' => $this->getIfDesc($nexthop['via'] ?? $nexthop['directly attached to']),
];
}
}
return $this->searchRecordsetBase($records);
}
public function ospfdatabaseAction($format = "json"): array
{
return $this->getInformation("ospf", "database", $format);
@@ -29,12 +29,71 @@ class DiagnosticsController extends \OPNsense\Base\IndexController
{
public function bgpAction()
{
$this->view->diagnosticsForm = $this->getForm("diagnostics");
$this->view->pick('OPNsense/Quagga/diagnosticsbgp');
$this->view->tabs = [
[
'name' => 'routing4',
'endpoint' => '/api/quagga/diagnostics/search_bgproute4',
'tabhead' => "IPv4 " . gettext('Routing Table'),
'type' => 'bgproutetable'
],
[
'name' => 'routing6',
'endpoint' => '/api/quagga/diagnostics/search_bgproute6',
'tabhead' => "IPv6 " . gettext('Routing Table'),
'type' => 'bgproutetable'
],
[
'name' => 'neighbors',
'endpoint' => '/api/quagga/diagnostics/bgpneighbors',
'tabhead' => gettext('Neighbors'),
'type' => 'tree'
],
[
'name' => 'summary',
'endpoint' => '/api/quagga/diagnostics/bgpsummary',
'tabhead' => gettext('Summary'),
'type' => 'tree'
]
];
$this->view->default_tab = 'routing4';
$this->view->pick('OPNsense/Quagga/diagnostics');
}
public function ospfAction()
{
$this->view->pick('OPNsense/Quagga/diagnosticsospf');
$this->view->tabs = [
[
'name' => 'overview',
'endpoint' => '/api/quagga/diagnostics/ospfoverview',
'tabhead' => gettext('Overview'),
'type' => 'tree'
],
[
'name' => 'routing',
'endpoint' => '/api/quagga/diagnostics/search_ospfroute',
'tabhead' => gettext('Routing Table'),
'type' => 'ospfroutetable'
],
[
'name' => 'database',
'endpoint' => '/api/quagga/diagnostics/ospfdatabase/plain',
'tabhead' => gettext('Database'),
'type' => 'text'
],
[
'name' => 'neighbors',
'endpoint' => '/api/quagga/diagnostics/search_ospfneighbor',
'tabhead' => gettext('Neighbors'),
'type' => 'ospfneighbors'
],
[
'name' => 'interfaces',
'endpoint' => '/api/quagga/diagnostics/ospfinterface',
'tabhead' => gettext('Interfaces'),
'type' => 'tree'
]
];
$this->view->default_tab = 'routing';
$this->view->pick('OPNsense/Quagga/diagnostics');
}
public function ospfv3Action()
{
@@ -42,6 +101,27 @@ class DiagnosticsController extends \OPNsense\Base\IndexController
}
public function generalAction()
{
$this->view->pick('OPNsense/Quagga/diagnosticsgeneral');
$this->view->tabs = [
[
'name' => 'routing4',
'endpoint' => '/api/quagga/diagnostics/search_generalroute4',
'tabhead' => gettext('IPv4 Routes'),
'type' => 'generalroutetable'
],
[
'name' => 'routing6',
'endpoint' => '/api/quagga/diagnostics/search_generalroute6',
'tabhead' => gettext('IPv6 Routes'),
'type' => 'generalroutetable'
],
[
'name' => 'runningconfig',
'endpoint' => '/api/quagga/diagnostics/generalrunningconfig/plain',
'tabhead' => gettext('Running Configuration'),
'type' => 'text'
]
];
$this->view->default_tab = 'routing4';
$this->view->pick('OPNsense/Quagga/diagnostics');
}
}
@@ -1,8 +0,0 @@
<form>
<field>
<id>diagnostics.bgpneighbor</id>
<label>BGP Neighbor</label>
<type>text</type>
<hint>One of the neighbor IPs</hint>
</field>
</form>
@@ -0,0 +1,341 @@
{#
OPNsense® is Copyright © 2014 2023 by Deciso B.V.
Copyright (C) 2023 Marc Bartelt
Copyright (C) 2017 Fabian Franz
Copyright (C) 2017 Michael Muenz <m.muenz@gmail.com>
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 src="{{ cache_safe('/ui/js/quagga/diagnostics_utils.js') }}"></script>
<style>
.searchbox {
margin: 8px;
}
.node-selected {
font-weight: bolder;
}
#page_frr_subtitle {
margin-left: -2px;
> span {
font-style: italic;
}
}
</style>
<link rel="stylesheet" type="text/css" href="{{ cache_safe(theme_file_or_default('/css/jqtree.css', ui_theme|default('opnsense'))) }}">
<script src="{{ cache_safe('/ui/js/tree.jquery.min.js') }}"></script>
<script>
'use strict';
$( document ).ready(function() {
updateServiceControlUI('quagga');
/**
* only display the refresh button on the currently active tab
*/
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$(".tab-icon").removeClass("fa-refresh");
$("#"+e.target.id).find(".tab-icon").addClass("fa-refresh");
});
/**
* resize tree widgets on window resize
*/
$(window).on('resize', resizeTreeWidget);
/**
* delayed search for tree widgets
*/
$(".tree_search").keyup(treeSearchKeyUp);
let all_grids = [];
{% for tab in tabs %}
/**
* register refresh event handler for {{ tab['tabhead'] }}
*/
$("#refresh-{{ tab['name'] }}").click(function () {
{% switch tab['type'] %}
{% case 'generalroutetable' %}
{% case 'bgproutetable' %}
{% case 'ospfroutetable' %}
{% case 'ospfneighbors' %}
if (all_grids["{{ tab['name'] }}"] === undefined) {
/**
* initialize bootgrid table for {{ tab['tabhead'] }}
*/
gridopt = {
search: "{{ tab['endpoint'] }}",
options:{
formatters : {
general_route_code: function(column, row){
let protocols = {
'kernel' : {short: 'K', long: '{{ lang._('Kernel') }}'},
'connected': {short: 'C', long: '{{ lang._('Connected') }}'},
'bgp': {short: 'B', long: '{{ lang._('BGP') }}'},
'ospf': {short: 'O', long: '{{ lang._('OSPF') }}'},
'ospf6': {short: 'O', long: '{{ lang._('OSPFv3') }}'}
};
let field = $("<div/>");
if (protocols[row.protocol] !== undefined) {
let tmp = protocols[row.protocol];
field.append($("<abbr>").text(tmp.short).attr('title', tmp.long));
}
if(row.selected) {
field.append($("<abbr>").html("&gt;").attr('title', "{{ lang._('Selected') }}"));
}
if(row.installed) {
field.append($("<abbr>").html("&ast;").attr('title', "{{ lang._('FIB') }}"));
}
return field.html();
},
boolean: function (column, row) {
if (row[column.id]) {
return "<span class=\"fa fa-check\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
} else {
return "<span class=\"fa fa-times\" data-value=\"0\" data-row-id=\"" + row.uuid + "\"></span>";
}
},
origin: function(column, row) {
return (row[column.id] === 'incomplete' ? '<abbr title="{{ lang._('Incomplete') }}">&quest;</abbr>' : row[column.id]);
}
},
responseHandler: function (data) {
if (data.subtitle !== undefined) {
$("#page_frr_subtitle").empty();
$("#page_frr_subtitle").append('<i class="fa fa-fw fa-chevron-right" aria-hidden="true"></i>');
$("#page_frr_subtitle").append($("<span/>").text(data.subtitle));
}
console.log(data);
return data;
}
}
};
all_grids["{{ tab['name'] }}"] = $("#grid-{{ tab['name'] }}").UIBootgrid(gridopt);
all_grids["{{ tab['name'] }}"].on("loaded.rs.jquery.bootgrid", function (e) {
$("abbr").tooltip();
});
} else {
all_grids["{{ tab['name'] }}"].bootgrid('reload');
}
{% break %}
{% case 'tree' %}
ajaxGet("{{ tab['endpoint'] }}", {}, function (data, status) {
if (status == "success") {
let $tree = $("#tree-{{ tab['name'] }}");
if ($("#tree-{{ tab['name'] }} > ul").length == 0) {
$tree.tree({
data: dict_to_tree(data['response']),
autoOpen: false,
dragAndDrop: false,
selectable: false,
closedIcon: $('<i class="fa fa-plus-square-o"></i>'),
openedIcon: $('<i class="fa fa-minus-square-o"></i>'),
onCreateLi: function(node, $li) {
let n_title = $li.find('.jqtree-title');
n_title.text(n_title.text().replace('&gt;','\>').replace('&lt;','\<'));
if (node.value !== undefined) {
$li.find('.jqtree-element').append(
'&nbsp; <strong>:</strong> &nbsp;' + node.value
);
}
if (node.selected) {
$li.addClass("node-selected");
} else {
$li.removeClass("node-selected");
}
}
});
// initial view, collapse first level if there's only one node
if (Object.keys(data['response']).length == 1) {
for (let key in data['response']) {
$tree.tree('openNode', $tree.tree('getNodeById', key));
}
}
//open node on label click
$tree.bind('tree.click', function(e) {
$tree.tree('toggle', e.node);
});
} else {
let curent_state = $tree.tree('getState');
$tree.tree('loadData', dict_to_tree(data['response']));
$tree.tree('setState', curent_state);
}
}
});
$(window).trigger('resize');
{% break %}
{% case 'text' %}
ajaxGet("{{ tab['endpoint'] }}", {}, function(data, status) {
if (status == "success") {
$('#text-{{ tab['name'] }}').html(data['response']);
}
});
{% break %}
{% endswitch %}
});
/**
* perform data fetch via event handler
*/
$("a[id='{{ tab['name'] }}_tab']").on("shown.bs.tab", function (event) {
$("#refresh-{{ tab['name'] }}").click();
});
{% endfor %}
/**
* add "sub title" heading
*/
$("header.page-content-head > div:eq(0) > ul:eq(0) > li:eq(0)").append($("<span id='page_frr_subtitle'/>"));
/**
* activate the default tab
*/
$("a[id='{{ default_tab }}_tab']").click();
});
</script>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
{% for tab in tabs %}
<li>
<a data-toggle="tab" href="#{{ tab['name'] }}" id="{{tab['name']}}_tab">
{{ tab['tabhead'] }} <span id="refresh-{{ tab['name'] }}" class="fa tab-icon fa-refresh" style="cursor: pointer"></span>
</a>
</li>
{% endfor %}
</ul>
<div class="content-box tab-content" style="padding-bottom: 1.5em;">
{% for tab in tabs %}
<div id="{{ tab['name'] }}" class="tab-pane fade">
{% switch tab['type'] %}
{% case 'generalroutetable' %}
<div class="col-sm-12">
<table id="grid-{{ tab['name'] }}" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="protocol" data-searchable="false" data-formatter="general_route_code" data-sortable="false">{{ lang._('Code') }}</th>
<th data-column-id="selected" data-searchable="false" data-visible="false" data-sortable="false">{{ lang._('Selected') }}</th>
<th data-column-id="installed" data-searchable="false" data-visible="false" data-sortable="false">{{ lang._('Installed') }}</th>
<th data-column-id="prefix">{{ lang._('Network') }}</th>
<th data-column-id="distance" data-searchable="false">{{ lang._('Administrative Distance') }}</th>
<th data-column-id="metric" data-searchable="false">{{ lang._('Metric') }}</th>
<th data-column-id="interfaceName">{{ lang._('Interface') }}</th>
<th data-column-id="interfaceDescr">{{ lang._('Interface name') }}</th>
<th data-column-id="via">{{ lang._('Via') }}</th>
<th data-column-id="uptime" data-searchable="false">{{ lang._('Time') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% break %}
{% case 'bgproutetable' %}
<div class="col-sm-12">
<table id="grid-{{ tab['name'] }}" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="valid" data-searchable="false" data-formatter="boolean" data-sortable="false">{{ lang._('Valid') }}</th>
<th data-column-id="bestpath" data-searchable="false" data-formatter="boolean" data-sortable="false">{{ lang._('Best') }}</th>
<th data-column-id="internal" data-searchable="false" data-formatter="boolean" data-sortable="false">{{ lang._('Internal') }}</th>
<th data-column-id="network" data-width="15%">{{ lang._('Network') }}</th>
<th data-column-id="ip" data-width="25%">{{ lang._('Next Hop') }}</th>
<th data-column-id="metric" data-searchable="false">{{ lang._('Metric') }}</th>
<th data-column-id="locPrf" data-searchable="false">{{ lang._('LocPrf') }}</th>
<th data-column-id="weight" data-searchable="false">{{ lang._('Weight') }}</th>
<th data-column-id="path" data-width="21%">{{ lang._('Path') }}</th>
<th data-column-id="origin" data-width="10%" data-formatter="origin">{{ lang._('Origin') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% break %}
{% case 'ospfroutetable' %}
<div class="col-sm-12">
<table id="grid-{{ tab['name'] }}" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="type" data-searchable="false" data-formatter="ospf_route_type" data-sortable="false">{{ lang._('Type') }}</th>
<th data-column-id="network">{{ lang._('Network') }}</th>
<th data-column-id="cost" data-searchable="false">{{ lang._('Cost') }}</th>
<th data-column-id="area">{{ lang._('Area') }}</th>
<th data-column-id="via">{{ lang._('Via') }}</th>
<th data-column-id="viainterface">{{ lang._('Via interface') }}</th>
<th data-column-id="viainterfaceDescr">{{ lang._('Via interface name') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% break %}
{% case 'ospfneighbors' %}
<div class="col-sm-12">
<table id="grid-{{ tab['name'] }}" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="neighborid">{{ lang._('Neighbor ID') }}</th>
<th data-column-id="priority" data-searchable="false">{{ lang._('Priority') }}</th>
<th data-column-id="state">{{ lang._('State') }}</th>
<th data-column-id="deadTimeMsecs" data-searchable="false">{{ lang._('Dead Time') }} &lsqb;ms&rsqb;</th>
<th data-column-id="address">{{ lang._('Address') }}</th>
<th data-column-id="ifaceName">{{ lang._('Interface') }}</th>
<th data-column-id="retransmitCounter" data-searchable="false">{{ lang._('Retransmit Counter') }}</th>
<th data-column-id="requestCounter" data-searchable="false">{{ lang._('Request Counter') }}</th>
<th data-column-id="dbSummaryCounter" data-searchable="false">{{ lang._('DB Summary Counter') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% break %}
{% case 'tree' %}
<div class="searchbox">
<input
id="search-{{ tab['name'] }}"
type="text"
for="tree-{{tab['name']}}"
class="tree_search"
placeholder="{{ lang._('search') }}"
></input>
</div>
<div class="treewidget" style="padding: 8px; overflow-y: scroll; height:400px;" id="tree-{{ tab['name'] }}"></div>
{% break %}
{% case 'text' %}
<pre id="text-{{ tab['name'] }}"></pre>
{% break %}
{% endswitch %}
</div>
{% endfor %}
</div>
@@ -1,143 +0,0 @@
{#
OPNsense® is Copyright © 2014 2017 by Deciso B.V.
Copyright (C) 2017 Fabian Franz
Copyright (C) 2017 Michael Muenz <m.muenz@gmail.com>
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.
#}
{#
{{ partial("layout_partials/base_form",['fields':diagnosticsForm,'id':'frm_diagnostics_settings'])}}
#}
<script type="text/x-template" id="routestpl">
<h2>{{ lang._('Table Version') }}: <%= tableVersion %></h2>
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="valid" data-type="boolean" data-formatter="boolean" data-width="5%">{{ lang._('Valid') }}</th>
<th data-column-id="best" data-type="boolean" data-formatter="boolean" data-width="5%">{{ lang._('Best') }}</th>
<th data-column-id="internal" data-type="boolean" data-formatter="boolean" data-width="6%">{{ lang._('Internal') }}</th>
<th data-column-id="network" data-type="string" data-width="21%">{{ lang._('Network') }}</th>
<th data-column-id="nexthop" data-type="string" data-width="21%">{{ lang._('Next Hop') }}</th>
<th data-column-id="metric" data-type="numeric" data-width="5%">{{ lang._('Metric') }}</th>
<th data-column-id="locprf" data-type="numeric" data-width="5%">{{ lang._('LocPrf') }}</th>
<th data-column-id="weight" data-type="numeric" data-width="6%">{{ lang._('Weight') }}</th>
<th data-column-id="path" data-type="string" data-width="16%">{{ lang._('Path') }}</th>
<th data-column-id="origin" data-type="string" data-formatter="origin" data-width="10%">{{ lang._('Origin') }}</th>
</tr>
</thead>
<tbody>
<% _.forEach(routes, function(route_array, network) { %>
<% _.forEach(route_array, function(route) { %>
<% _.forEach(route['nexthops'], function(nexthop) { %>
<tr>
<td><%= (typeof(route['valid']) != "undefined" && route['valid']) %></td>
<td><%= (typeof(route['bestpath']) != "undefined" && route['bestpath']) %></td>
<td><%= (typeof(route['pathFrom']) != "undefined" && route['pathFrom'] == 'internal') %></td>
<td><%= network %></td>
<td><%= nexthop['ip'] %></td>
<td><%= route['metric'] %></td>
<td><%= route['locPrf'] %></td>
<td><%= route['weight'] %></td>
<td><%= (route['path'] == "" ? "{{ lang._('Internal') }}" : route['path']) %></td>
<td><%= route['origin'] %></td>
</tr>
<% }); %>
<% }); %>
<% }); %>
</tbody>
</table>
</script>
<script type="text/javascript" src="/ui/js/quagga/lodash.js"></script>
<script type="text/javascript">
let dataconverters = {
boolean: {
from: function (value) { return (value == 'true') || (value == true); },
to: function (value) { return value; }
}
}
let dataformatters = {
boolean: function (column, row) {
if (row[column.id]) {
return "<span class=\"fa fa-check\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
} else {
return "<span class=\"fa fa-times\" data-value=\"0\" data-row-id=\"" + row.uuid + "\"></span>";
}
},
origin: function(column, row) {
return (row[column.id] === 'incomplete' ? '<abbr title="{{ lang._('Incomplete') }}">&quest;</abbr>' : row[column.id]);
}
}
$(document).ready(function() {
updateServiceControlUI('quagga');
ajaxCall(url="/api/quagga/diagnostics/bgproute4", sendData={}, callback=function(data, status) {
content = _.template($('#routestpl').html())(data['response']);
$('#routing').html(content);
$('#routing table').bootgrid({
formatters: dataformatters
});
});
ajaxCall(url="/api/quagga/diagnostics/bgproute6", sendData={}, callback=function(data, status) {
content = _.template($('#routestpl').html())(data['response']);
$('#routing6').html(content);
$('#routing6 table').bootgrid({
formatters: dataformatters
});
});
ajaxCall(url="/api/quagga/diagnostics/bgpneighbors/plain", sendData={}, callback=function(data, status) {
$('#neighborscontent').text(data['response']);
});
ajaxCall(url="/api/quagga/diagnostics/bgpsummary/plain", sendData={}, callback=function(data, status) {
$("#summarycontent").text(data['response']);
});
});
</script>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#routing">IPv4 {{ lang._('Routing Table') }}</a></li>
<li><a data-toggle="tab" href="#routing6">IPv6 {{ lang._('Routing Table') }}</a></li>
<li><a data-toggle="tab" href="#neighbors">{{ lang._('Neighbors') }}</a></li>
<li><a data-toggle="tab" href="#summary">{{ lang._('Summary') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="routing" class="tab-pane fade in active"></div>
<div id="routing6" class="tab-pane fade in"></div>
<div id="neighbors" class="tab-pane fade in">
<pre id="neighborscontent"></pre>
</div>
<div id="summary" class="tab-pane fade in">
<pre id="summarycontent"></pre>
</div>
</div>
@@ -1,142 +0,0 @@
{#
OPNsense® is Copyright © 2014 2017 by Deciso B.V.
Copyright (C) 2017 Fabian Franz
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 src="/ui/js/quagga/lodash.js"></script>
<script type="text/x-template" id="routestpl">
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="code" data-type="string" data-formatter="code">{{ lang._('Code') }}</th>
<th data-column-id="selected" data-type="boolean" data-visible="false">{{ lang._('Selected') }}</th>
<th data-column-id="installed" data-type="boolean" data-visible="false">{{ lang._('Installed') }}</th>
<th data-column-id="network" data-type="string">{{ lang._('Network') }}</th>
<th data-column-id="ad" data-type="numeric">{{ lang._('Administrative Distance') }}</th>
<th data-column-id="metric" data-type="numeric">{{ lang._('Metric') }}</th>
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="via" data-type="string">{{ lang._('Via') }}</th>
<th data-column-id="time" data-type="string">{{ lang._('Time') }}</th>
</tr>
</thead>
<tbody>
<% _.forEach(routes, function(route_array, network) { %>
<% _.forEach(route_array, function(route) { %>
<% _.forEach(route['nexthops'], function(nexthop) { %>
<tr>
<td><%= route['protocol'] %></td>
<td><%= (typeof(route['selected']) != "undefined" && route['selected']) %></td>
<td><%= (typeof(route['installed']) != "undefined" && route['installed']) %></td>
<td><%= network %></td>
<td><%= route['distance'] %></td>
<td><%= route['metric'] %></td>
<td><%= nexthop['interfaceName'] %></td>
<td><%= (typeof(nexthop['ip']) != "undefined" ? nexthop['ip'] : '{{ lang._('Directly Attached') }}') %></td>
<td><%= route['uptime'] %></td>
</tr>
<% }); %>
<% }); %>
<% }); %>
</tbody>
</table>
</script>
<script type="text/javascript">
function translateProtocol(data) {
let tr = [];
// routing table tab
tr['kernel'] = {short: 'K', long: '{{ lang._('Kernel') }}'};
tr['connected'] = {short: 'C', long: '{{ lang._('Connected') }}'};
tr['bgp'] = {short: 'B', long: '{{ lang._('BGP') }}'};
tr['ospf'] = {short: 'O', long: '{{ lang._('OSPF') }}'};
tr['ospf6'] = {short: 'O', long: '{{ lang._('OSPFv3') }}'};
return _.has(tr,data) ? tr[data] : data;
}
let dataconverters = {
boolean: {
from: function (value) { return (value == 'true') || (value == true); },
to: function (value) { return value; }
}
}
let dataformatters = {
code: function(column, row) {
let result = row.code;
let protocol = translateProtocol(row.code);
if(typeof(protocol) != "string") result = '<abbr title="' + protocol['long'] + '">' + protocol['short'] + '</abbr> ';
if(row.selected) result += '<abbr title="{{ lang._('Selected') }}">&gt;</abbr> ';
if(row.installed) result += '<abbr title="{{ lang._('FIB') }}">&ast;</abbr>';
return result;
}
};
$(document).ready(function() {
updateServiceControlUI('quagga');
ajaxCall(url="/api/quagga/diagnostics/generalroute", sendData={}, callback=function(data, status) {
let content = _.template($('#routestpl').html())({
routes: data['response']['ipv4']
});
$('#routing').html(content);
$('#routing table').bootgrid({
converters: dataconverters,
formatters: dataformatters
});
content = _.template($('#routestpl').html())({
routes: data['response']['ipv6']
});
$('#routing6').html(content);
$('#routing6 table').bootgrid({
converters: dataconverters,
formatters: dataformatters
});
});
ajaxCall(url="/api/quagga/diagnostics/generalrunningconfig", sendData={}, callback=function(data, status) {
$("#runningconfig").text(data['response']);
});
});
</script>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#routing">{{ lang._('IPv4 Routes') }}</a></li>
<li><a data-toggle="tab" href="#routing6">{{ lang._('IPv6 Routes') }}</a></li>
<li><a data-toggle="tab" href="#showrun">{{ lang._('Running Configuration') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="routing" class="tab-pane fade in active"></div>
<div id="routing6" class="tab-pane fade in"></div>
<div id="showrun" class="tab-pane fade in">
<pre id="runningconfig"></pre>
</div>
</div>
@@ -1,475 +0,0 @@
{#
OPNsense® is Copyright © 2014 2017 by Deciso B.V.
Copyright (C) 2017 Fabian Franz
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 type="text/x-template" id="overviewtpl">
<h2>{{ lang._('General') }}</h2>
<table class="table table-striped">
<tbody>
<tr>
<td>{{ lang._('RFC2328 Conform') }}</td>
<td><%= checkmark(rfc2328Conform) %></td>
</tr>
<tr>
<td>{{ lang._('ASBR') }}</td>
<td><%= checkmark(typeof asbrRouter != 'undefined' && asbrRouter == "injectingExternalRoutingInformation") %></td>
</tr>
<tr>
<td>{{ lang._('Router ID') }}</td>
<td><%= routerId %></td>
</tr>
<tr>
<td>{{ lang._('RFC1583 Compatibility') }}</td>
<td><%= checkmark(typeof rfc1583Compatibility != "undefined" && rfc1583Compatibility) %></td>
</tr>
<tr>
<td>{{ lang._('Opaque Capability') }}</td>
<td><%= checkmark(typeof opaqueCapable != "undefined" && opaqueCapable) %></td>
</tr>
<tr>
<td>{{ lang._('Initial SPF Scheduling Delay') }}</td>
<td><%= spfScheduleDelayMsecs %> {{ lang._('Milliseconds') }}</td>
</tr>
<tr>
<td>{{ lang._('Minimum Hold Time') }}</td>
<td><%= holdtimeMinMsecs %> {{ lang._('Milliseconds') }}</td>
</tr>
<tr>
<td>{{ lang._('Maximum Hold Time') }}</td>
<td><%= holdtimeMaxMsecs %> {{ lang._('Milliseconds') }}</td>
</tr>
<tr>
<td>{{ lang._('Current Hold Time Multipier') }}</td>
<td><%= holdtimeMultplier %></td>
</tr>
<tr>
<td>{{ lang._('Refresh Timer') }}</td>
<td><%= refreshTimerMsecs %> {{ lang._('Milliseconds') }}</td>
</tr>
<tr>
<td>{{ lang._('Areas Attached Count') }}</td>
<td><%= attachedAreaCounter %></td>
</tr>
</tbody>
</table>
<h2>{{ lang._('Link State Area') }}</h2>
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>{{ lang._('Count') }}</th>
<th>{{ lang._('Checksum') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ lang._('External LSA') }}</td>
<td><%= lsaExternalCounter %></td>
<td><%= lsaExternalChecksum %></td>
</tr>
<tr>
<td>{{ lang._('Opaque AS LSA') }}</td>
<td><%= lsaAsopaqueCounter %></td>
<td><%= lsaAsOpaqueChecksum %></td>
</tr>
</tbody>
</table>
<% if (areas) { %>
<h2>{{ lang._('Areas') }}</h2>
<% _.forEach(areas, function(area, areaname) { %>
<br />
<table class="table table-striped">
<thead>
<tr>
<th><%= areaname %></th>
<th>{{ lang._('Count') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ lang._('Interfaces: Total') }}</td>
<td><%= area['areaIfTotalCounter'] %></td>
</tr>
<tr>
<td>{{ lang._('Interfaces: Active') }}</td>
<td><%= area['areaIfActiveCounter'] %></td>
</tr>
<tr>
<td>{{ lang._('Fully Adjacent Neighbor Count') }}</td>
<td><%= area['nbrFullAdjacentCounter'] %></td>
</tr>
<tr>
<td>{{ lang._('SPF Execution Count') }}</td>
<td><%= area['spfExecutedCounter'] %></td>
</tr>
</tbody>
</table>
<table class="table table-striped">
<thead>
<tr>
<th>{{ lang._('LSA Type') }}</th>
<th>{{ lang._('Count') }}</th>
<th>{{ lang._('Checksum') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ lang._('Router') }}</td>
<td><%= area['lsaRouterNumber'] %></td>
<td><%= area['lsaRouterChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('Network') }}</td>
<td><%= area['lsaNetworkNumber'] %></td>
<td><%= area['lsaNetworkChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('Summary') }}</td>
<td><%= area['lsaSummaryNumber'] %></td>
<td><%= area['lsaSummaryChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('ASBR Summary') }}</td>
<td><%= area['lsaAsbrNumber'] %></td>
<td><%= area['lsaAsbrChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('NSSA') }}</td>
<td><%= area['lsaNssaNumber'] %></td>
<td><%= area['lsaNssaChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('Opaque Link') }}</td>
<td><%= area['lsaOpaqueLinkNumber'] %></td>
<td><%= area['lsaOpaqueLinkChecksum'] %></td>
</tr>
<tr>
<td>{{ lang._('Opaque Area') }}</td>
<td><%= area['lsaOpaqueAreaNumber'] %></td>
<td><%= area['lsaOpaqueAreaNumber'] %></td>
</tr>
</tbody>
</table>
<% }); %>
<% } %>
</script>
<script type="text/x-template" id="databasetpl">
<% _.each(_.keys(ospf_database), function(router_id) { %>
<h1>{{ lang._('Router ID:')}} <%= router_id %></h1>
<hr />
<h2>{{ lang._('Router Link State Area') }}</h2>
<% _.each(_.keys(ospf_database[router_id]['router_link_state_area']), function(area) { %>
<h3>Area <%= area %></h3>
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="linkid" data-type="string">{{ lang._('Link ID') }}</th>
<th data-column-id="advrouter" data-type="string">{{ lang._('ADV Router') }}</th>
<th data-column-id="age" data-type="numeric">{{ lang._('Age') }}</th>
<th data-column-id="seqnr" data-type="string">{{ lang._('Sequence Number') }}</th>
<th data-column-id="cksum" data-type="string">{{ lang._('Checksum') }}</th>
<th data-column-id="linkcnt" data-type="numeric">{{ lang._('Link Count') }}</th>
</tr>
</thead>
<tbody>
<% _.each(ospf_database[router_id]['router_link_state_area'][area], function(entry) { %>
<tr>
<td><%= entry["Link ID"] %></td>
<td><%= entry["ADV Router"] %></td>
<td><%= entry["Age"] %></td>
<td><%= entry["Seq#"] %></td>
<td><%= entry["CkSum"] %></td>
<td><%= entry["Link count"] %></td>
</tr>
<% }); %>
</tbody>
</table>
<% }); %>
<h2>{{ lang._('Net Link State Area') }}</h2>
<% _.each(_.keys(ospf_database[router_id]['net_link_state_area']), function(area) { %>
<h3>{{ lang._('Area:') }} <%= area %></h3>
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="linkid" data-type="string">{{ lang._('Link ID') }}</th>
<th data-column-id="advrouter" data-type="string">{{ lang._('ADV Router') }}</th>
<th data-column-id="age" data-type="numeric">{{ lang._('Age') }}</th>
<th data-column-id="seqnr" data-type="string">{{ lang._('Sequence Number') }}</th>
<th data-column-id="cksum" data-type="string">{{ lang._('Checksum') }}</th>
</tr>
</thead>
<tbody>
<% _.each(ospf_database[router_id]['net_link_state_area'][area], function(entry) { %>
<tr>
<td><%= entry["Link ID"] %></td>
<td><%= entry["ADV Router"] %></td>
<td><%= entry["Age"] %></td>
<td><%= entry["Seq#"] %></td>
<td><%= entry["CkSum"] %></td>
</tr>
<% }); %>
</tbody>
</table>
<% }); %>
<h2>{{ lang._('External States') }}</h2>
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="linkid" data-type="string">{{ lang._('Link ID') }}</th>
<th data-column-id="advrouter" data-type="string">{{ lang._('ADV Router') }}</th>
<th data-column-id="age" data-type="numeric">{{ lang._('Age') }}</th>
<th data-column-id="seqnr" data-type="string">{{ lang._('Sequence Number') }}</th>
<th data-column-id="chsum" data-type="string">{{ lang._('Checksum') }}</th>
<th data-column-id="route" data-type="string">{{ lang._('Route') }}</th>
</tr>
</thead>
<tbody>
<% _.each(ospf_database[router_id]['external_states'], function(entry) { %>
<tr>
<td><%= entry["Link ID"] %></td>
<td><%= entry["ADV Router"] %></td>
<td><%= entry["Age"] %></td>
<td><%= entry["Seq#"] %></td>
<td><%= entry["CkSum"] %></td>
<td><%= entry["Route"] %></td>
</tr>
<% }); %>
</tbody>
</table>
<% }); %>
</script>
<script type="text/x-template" id="routestpl">
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="type" data-type="string" data-formatter="route_type">{{ lang._('Type') }}</th>
<th data-column-id="network" data-type="string">{{ lang._('Network') }}</th>
<th data-column-id="cost" data-type="numeric">{{ lang._('Cost') }}</th>
<th data-column-id="area" data-type="string">{{ lang._('Area') }}</th>
<th data-column-id="via" data-type="string">{{ lang._('Via') }}</th>
<th data-column-id="viainterface" data-type="string">{{ lang._('Via interface') }}</th>
</tr>
</thead>
<tbody>
<% _.forEach(routes, function(route, network) { %>
<tr>
<td><%= route['routeType'] %></td>
<td><%= network %></td>
<td><%= route['cost'] %></td>
<td><%= route['area'] %></td>
<td><%= (typeof route['nexthops'][0]['via'] != "undefined" ? route['nexthops'][0]['ip'] : translate('directly attached')) %></td>
<td><%= (typeof route['nexthops'][0]['via'] != "undefined" ? route['nexthops'][0]['via'] : route['nexthops'][0]['directly attached to']) %></td>
</tr>
<% route['nexthops'].shift(); %>
<% _.forEach(route['nexthops'], function(nexthop) { %>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td><%= (typeof nexthop['via'] != "undefined" ? nexthop['ip'] : translate('directly attached')) %></td>
<td><%= (typeof nexthop['via'] != "undefined" ? nexthop['via'] : nexthop['directly attached to']) %></td>
</tr>
<% }); %>
<% }); %>
</tbody>
</table>
</script>
<script type="text/x-template" id="neighbortpl">
<table class="table table-striped">
<thead>
<tr>
<th data-column-id="neighborid" data-type="string">{{ lang._('Neighbor ID') }}</th>
<th data-column-id="priority" data-type="numeric">{{ lang._('Priority') }}</th>
<th data-column-id="state" data-type="string">{{ lang._('State') }}</th>
<th data-column-id="deadtime" data-type="string">{{ lang._('Dead Time') }} &lsqb;ms&rsqb;</th>
<th data-column-id="address" data-type="string">{{ lang._('Address') }}</th>
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="rxmtl" data-type="numeric">{{ lang._('Retransmit Counter') }}</th>
<th data-column-id="rqstl" data-type="numeric">{{ lang._('Request Counter') }}</th>
<th data-column-id="dbsml" data-type="numeric">{{ lang._('DB Summary Counter') }}</th>
</tr>
</thead>
<tbody>
<% _.forEach(neighbors, function(connections, neighborId) { %>
<% _.forEach(connections, function(connection) { %>
<tr>
<td><%= neighborId %></td>
<td><%= connection['priority'] %></td>
<td><%= translate(connection['state']) %></td>
<td><%= connection['deadTimeMsecs'] %></td>
<td><%= connection['address'] %></td>
<td><%= connection['ifaceName'] %></td>
<td><%= connection['retransmitCounter'] %></td>
<td><%= connection['requestCounter'] %></td>
<td><%= connection['dbSummaryCounter'] %></td>
</tr>
<% }); %>
<% }); %>
</tbody>
</table>
</script>
<script type="text/x-template" id="interfacetpl">
<% _.forEach(interfaces, function(interface, interfacename) { %>
<h2><%= interfacename %></h2>
<table class="table table-striped">
<tbody>
<% _.forEach(interface, function(propertyvalue, propertyname) { %>
<tr>
<td><%= translate(propertyname) %></td>
<td>
<% if (typeof(propertyvalue) == "boolean") { %>
<%= checkmark(propertyvalue) %>
<% } else { %>
<%= translate(propertyvalue) %>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
<% }); %>
</script>
<script type="text/javascript" src="/ui/js/quagga/lodash.js"></script>
<script type="text/javascript">
function translate(data) {
let tr = [];
// routing table tab
tr['N'] = '{{ lang._('Network') }}';
tr['R'] = '{{ lang._('Router') }}';
tr['IA'] = '{{ lang._('OSPF inter area') }}';
tr['N1'] = '{{ lang._('OSPF NSSA external type 1') }}';
tr['N2'] = '{{ lang._('OSPF NSSA external type 2') }}';
tr['E1'] = '{{ lang._('OSPF external type 1') }}';
tr['E2'] = '{{ lang._('OSPF external type 2') }}';
tr['directly attached'] = '{{ lang._('Directly Attached') }}';
// neighbor tab
tr['Full/DR'] = '{{ lang._('Full (Designated Router)') }}';
// interfaces tab
tr['ifUp'] = '{{ lang._('Up') }}';
tr['ifIndex'] = '{{ lang._('Index') }}';
tr['mtuBytes'] = '{{ lang._('MTU') }} &lsqb;{{ lang._('Bytes') }}&rsqb;';
tr['bandwidthMbit'] = '{{ lang._('Bandwidth') }} &lsqb;Mbit/s&rsqb;';
tr['ifFlags'] = '{{ lang._('Flags') }}';
tr['ospfEnabled'] = '{{ lang._('OSPF Enabled') }}';
tr['ipAddress'] = '{{ lang._('Address') }}';
tr['ipAddressPrefixlen'] = '{{ lang._('Prefix Length') }}';
tr['ospfIfType'] = '{{ lang._('Type') }}';
tr['localIfUsed'] = '{{ lang._('Local Interface') }}';
tr['area'] = '{{ lang._('Area') }}';
tr['routerId'] = '{{ lang._('Router ID') }}';
tr['networkType'] = '{{ lang._('Network Type') }}';
tr['cost'] = '{{ lang._('Cost') }}';
tr['transmitDelaySecs'] = '{{ lang._('Transmit Delay') }} &lsqb;s&rsqb;';
tr['state'] = '{{ lang._('State') }}';
tr['priority'] = '{{ lang._('Priority') }}';
tr['mcastMemberOspfAllRouters'] = '{{ lang._('Multicast') }} {{ lang._('Group') }} {{ lang._('Member') }} OSPFAllRouters';
tr['timerMsecs'] = '{{ lang._('Hello Timer') }} &lsqb;ms&rsqb;';
tr['timerDeadSecs'] = '{{ lang._('Dead Timer') }} &lsqb;s&rsqb;';
tr['timerWaitSecs'] = '{{ lang._('Wait Timer') }} &lsqb;s&rsqb;';
tr['timerRetransmitSecs'] = '{{ lang._('Retransmit Timer') }} &lsqb;s&rsqb;';
tr['timerPassiveIface'] = '{{ lang._('Passive Interface') }}';
tr['timerHelloInMsecs'] = '{{ lang._('Hello Due In') }} &lsqb;ms&rsqb;';
tr['nbrCount'] = '{{ lang._('Neighbor Count') }}';
tr['nbrAdjacentCount'] = '{{ lang._('Adjacent Neighbor Count') }}';
return _.has(tr,data) ? tr[data] : data;
}
function checkmark(bin) {
return "<i class=\"fa " + (bin ? "fa-check-square" : "fa-square") + " text-muted\"></i>";
}
dataformatters = {
route_type: function(column, row) {
let result = ''
_.forEach(row.type.split(' '), function(routeType) {
if(translate(routeType) == routeType) result += routeType;
else result += '<abbr title="' + translate(routeType) + '">' + routeType + '</abbr>';
result += ' ';
});
return result;
}
};
$(document).ready(function() {
updateServiceControlUI('quagga');
ajaxCall(url="/api/quagga/diagnostics/ospfoverview", sendData={}, callback=function(data, status) {
let content = _.template($('#overviewtpl').html())(data['response']);
$('#overview').html(content);
});
ajaxCall(url="/api/quagga/diagnostics/ospfdatabase", sendData={}, callback=function(data, status) {
let content = _.template($('#databasetpl').html())(data['response']);
$('#database').html(content);
});
ajaxCall(url="/api/quagga/diagnostics/ospfroute", sendData={}, callback=function(data, status) {
let content = _.template($('#routestpl').html())({
routes: data['response']
});
$('#routing').html(content);
$('#routing table').bootgrid({
formatters: dataformatters
});
});
ajaxCall(url="/api/quagga/diagnostics/ospfneighbor", sendData={}, callback=function(data, status) {
let content = _.template($('#neighbortpl').html())(data['response']);
$('#neighbor').html(content);
$('#neighbor table').bootgrid({
formatters: dataformatters
});
});
ajaxCall(url="/api/quagga/diagnostics/ospfinterface", sendData={}, callback=function(data, status) {
let content = _.template($('#interfacetpl').html())(data['response']);
$('#interface').html(content);
});
});
</script>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#overview">{{ lang._('Overview') }}</a></li>
<li><a data-toggle="tab" href="#routing">{{ lang._('Routing Table') }}</a></li>
<li><a data-toggle="tab" href="#database">{{ lang._('Database') }}</a></li>
<li><a data-toggle="tab" href="#neighbor">{{ lang._('Neighbor') }}</a></li>
<li><a data-toggle="tab" href="#interface">{{ lang._('Interface') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="overview" class="tab-pane fade in active"></div>
<div id="routing" class="tab-pane fade in"></div>
<div id="database" class="tab-pane fade in"></div>
<div id="neighbor" class="tab-pane fade in"></div>
<div id="interface" class="tab-pane fade in"></div>
</div>
@@ -404,7 +404,8 @@ class OSPFv3(Daemon):
self.myre.search(r'SPF algorithm last executed (.*)', line) or \
self.myre.search(r'Last SPF duration (.*)', line) or \
self.myre.search(r'SPF last executed (.*)', line) or \
self.myre.search(r'Number of Area scoped LSAs is (.*)', line):
self.myre.search(r'Number of Area scoped LSAs is (.*)', line) or \
self.myre.search(r'Adjacency changes are logged', line):
# skip these lines
pass
else:
@@ -0,0 +1,207 @@
/*
* Copyright (C) 2015-2022 Deciso B.V.
* Copyright (C) 2023 Marc Bartelt
* 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.
*/
'use strict';
/**
* shared options for bootgrid tables
*/
let gridopt = {
formatters: {
boolean: function(column, row) {
if (row[column.id]) {
return '<span class="fa fa-fw fa-check" data-value="1" data-row-id="' + row.uuid + '"></span>';
} else {
return '';
}
},
ospf_route_type: function(column, row) {
let result = '';
row[column.id].split(' ').forEach(function(routeType) {
let translatedRouteType = translateOSPFTerm(routeType)
if (translatedRouteType === routeType) {
result += routeType;
} else {
result += '<abbr title="' + translatedRouteType + '">' + routeType + '</abbr>';
}
result += ' ';
});
return result;
},
general_route_code: function(column, row) {
let result = row.code;
let protocol = translateZebraCode(row.code);
if(typeof(protocol) !== 'string') result = '<abbr title="' + protocol['long'] + '">' + protocol['short'] + '</abbr>';
if(row.selected) result += ' <abbr title="Selected">&gt;</abbr>';
if(row.installed) result += ' <abbr title="FIB">&ast;</abbr>';
return result;
}
}
};
/**
* zebra route codes - translation table
*/
function translateZebraCode(data) {
let tr = [];
// routing table tab
tr['kernel'] = {short: 'K', long: 'Kernel'};
tr['connected'] = {short: 'C', long: 'Connected'};
tr['bgp'] = {short: 'B', long: 'BGP'};
tr['ospf'] = {short: 'O', long: 'OSPF'};
tr['ospf6'] = {short: 'O', long: 'OSPFv3'};
return ((data in tr) ? tr[data] : data);
}
/**
* OSPF terms and abbreviations - translation table
**/
function translateOSPFTerm(data) {
let tr = [];
// routing table tab
tr['N'] = 'Network';
tr['R'] = 'Router';
tr['IA'] = 'OSPF inter area';
tr['N1'] = 'OSPF NSSA external type 1';
tr['N2'] = 'OSPF NSSA external type 2';
tr['E1'] = 'OSPF external type 1';
tr['E2'] = 'OSPF external type 2';
return ((data in tr) ? tr[data] : data);
}
/**
* tree view: resize widget on window resize
*/
function resizeTreeWidget() {
let new_height = $(".page-foot").offset().top -
($(".page-content-head").offset().top + $(".page-content-head").height()) - 160;
$(".treewidget").height(new_height);
$(".treewidget").css('max-height', new_height + 'px');
}
/**
* tree view: delayed live-search
*/
let apply_tree_search_timer = null;
function treeSearchKeyUp() {
let sender = $(this);
clearTimeout(apply_tree_search_timer);
apply_tree_search_timer = setTimeout(function() {
let searchTerm = sender.val().toLowerCase();
let target = $("#"+sender.attr('for'));
let tree = target.tree("getTree");
let selected = [];
if (tree !== null) {
tree.iterate((node) => {
let matched = false;
if (searchTerm !== "") {
matched = node.name.toLowerCase().includes(searchTerm);
if (!matched && typeof node.value === 'string') {
matched = node.value.toLowerCase().includes(searchTerm);
}
}
node["selected"] = matched;
if (matched) {
selected.push(node);
if (node.isFolder()) {
node.is_open = true;
}
let parent = node.parent;
while (parent) {
parent.is_open = true;
parent = parent.parent;
}
} else if (node.isFolder()) {
node.is_open = false;
}
return true;
});
target.tree("refresh");
if (selected.length > 0) {
target.tree('scrollToNode', selected[0]);
}
}
}, 500);
}
/**
* jqtree expects a list + dict type structure, transform key value store into expected output
* https://mbraak.github.io/jqTree/#general
*/
function dict_to_tree(node, path) {
// some entries are lists, try use a name for the nodes in that case
let node_name_keys = ['name', 'interface-name'];
let result = [];
if ( path === undefined) {
path = "";
} else {
path = path + ".";
}
for (let key in node) {
if (typeof node[key] === "function") {
continue;
}
let item_path = path + key;
if (node[key] instanceof Object) {
let node_name = key;
for (let idx=0; idx < node_name_keys.length; ++idx) {
if (/^(0|[1-9]\d*)$/.test(node_name) && node[key][node_name_keys[idx]] !== undefined) {
node_name = node[key][node_name_keys[idx]];
break;
}
}
result.push({
name: node_name,
id: item_path,
children: dict_to_tree(node[key], item_path)
});
} else {
result.push({
name: key,
value: node[key],
id: item_path
});
}
}
return result;
}