diff --git a/net/frr/Makefile b/net/frr/Makefile
index 1cad170a0..9c97041ea 100644
--- a/net/frr/Makefile
+++ b/net/frr/Makefile
@@ -1,7 +1,7 @@
PLUGIN_NAME= frr
-PLUGIN_VERSION= 1.20
+PLUGIN_VERSION= 1.21
PLUGIN_COMMENT= The FRRouting Protocol Suite
-PLUGIN_DEPENDS= frr7 ruby
+PLUGIN_DEPENDS= frr7
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com
.include "../../Mk/plugins.mk"
diff --git a/net/frr/pkg-descr b/net/frr/pkg-descr
index a25daaa3e..c0da55615 100644
--- a/net/frr/pkg-descr
+++ b/net/frr/pkg-descr
@@ -11,6 +11,27 @@ switching and routing, Internet access routers, and Internet peering.
Plugin Changelog
================
+1.21
+
+* Deprecate Ruby parser for FRR diagnostic output, remove Ruby dependency
+* Use FRR's json output where available
+* Introduce Python parser for FRR output where json is not (yet) available
+* Streamline diagnostic action names
+* Streamline diagnostic API endpoint names
+* Add diagnostic action and API endpoint for BGP neighbors
+* Webif Diagnostics/General: Fix routing tables
+* Webif Diagnostics/General: Align routing table style with other pages
+* Webif Diagnostics/OSPF: Correctly display areas in "Overview" tab
+* Webif Diagnostics/OSPF: Fix area rendering to "NaN" in "Route Table" tab
+* Webif Diagnostics/OSPF: Use only one table in "Route Table" tab
+* Webif Diagnostics/OSPF: Add mouseover hints to Type column in "Route Table" tab
+* Webif Diagnostics/OSPF: Slight presentation adjustments in "Neighbor" tab
+* Webif Diagnostics/OSPFv3: Fix age and sequence number rendering to "NaN" in "Database" tab
+* Webif Diagnostics/BGP: Rename page from "BGPv4" to just "BGP"
+* Webif Diagnostics/BGP: Replace "Overview" tab with distinct "IPv4 Routing Table" and "IPv6 Routing table" tabs
+* Webif Diagnostics/BGP: Align routing table style with other pages
+* Webif Diagnostics/BGP: Add Neighbors tab
+
1.20
* Allow to adjust reference cost for OSPF calculation
diff --git a/net/frr/src/opnsense/mvc/app/controllers/OPNsense/Quagga/Api/DiagnosticsController.php b/net/frr/src/opnsense/mvc/app/controllers/OPNsense/Quagga/Api/DiagnosticsController.php
index a6d8da8f4..7426a5f53 100644
--- a/net/frr/src/opnsense/mvc/app/controllers/OPNsense/Quagga/Api/DiagnosticsController.php
+++ b/net/frr/src/opnsense/mvc/app/controllers/OPNsense/Quagga/Api/DiagnosticsController.php
@@ -41,96 +41,111 @@ use OPNsense\Core\Config;
*/
class DiagnosticsController extends ApiControllerBase
{
- /**
- * show ip bgp
- * @return array
- */
- public function showipbgpAction()
+ private function getInformation(string $daemon, string $name, string $format): array
{
$backend = new Backend();
- $response = json_decode(trim($backend->configdRun("quagga diag-bgp2")));
- return array("response" => $response);
+ $response = $backend->configdRun("quagga diagnostics ".$daemon."_".$name.($format === "json" ? "_json" : ""));
+ return array("response" => ($format === "json" ? json_decode($response) : $response));
}
- /**
- * show ip bgp summary
- * @return array
- */
- public function showipbgpsummaryAction()
+
+ public function generalrunningconfigAction(): array
{
- $backend = new Backend();
- $response = $backend->configdRun("quagga diag-bgp summary");
- return array("response" => $response);
+ return $this->getInformation("general", "running-config", "plain");
}
- public function showrunningconfigAction()
+
+ public function generalrouteAction($format = "json"): array
{
- $backend = new Backend();
- $response = $backend->configdRun("quagga general-runningconfig");
- return array("response" => $response);
+ $routes4 = $this->getInformation("general", "route4", $format)['response'];
+ $routes6 = $this->getInformation("general", "route6", $format)['response'];
+ if ($format === "json") {
+ return array("response" => array("ipv4" => $routes4, "ipv6" => $routes6));
+ } else {
+ return array("response" => $routes4.$routes6);
+ }
}
- private function get_ospf_information($name)
+
+ public function generalroute4Action($format = "json"): array
{
- $backend = new Backend();
- return array("response" => json_decode(trim($backend->configdRun("quagga ospf-$name"))));
+ return $this->getInformation("general", "route4", $format);
}
- private function get_ospf3_information($name)
+
+ public function generalroute6Action($format = "json"): array
{
- $backend = new Backend();
- return array("response" => json_decode(trim($backend->configdRun("quagga ospfv3-$name"))));
+ return $this->getInformation("general", "route6", $format);
}
- // OSPFv2
- public function ospfoverviewAction()
+
+ public function bgprouteAction($format = "json"): array
{
- return $this->get_ospf_information('overview');
+ return $this->getInformation("bgp", "route", $format);
}
- public function ospfneighborAction()
+
+ public function bgproute4Action($format = "json"): array
{
- return $this->get_ospf_information('neighbor');
+ return $this->getInformation("bgp", "route4", $format);
}
- public function ospfrouteAction()
+
+ public function bgproute6Action($format = "json"): array
{
- return $this->get_ospf_information('route');
+ return $this->getInformation("bgp", "route6", $format);
}
- public function ospfdatabaseAction()
+
+ public function bgpsummaryAction($format = "json"): array
{
- return $this->get_ospf_information('database');
+ return $this->getInformation("bgp", "summary", $format);
}
- public function ospfinterfaceAction()
+
+ public function bgpneighborsAction($format = "json"): array
{
- return $this->get_ospf_information('interface');
+ return $this->getInformation("bgp", "neighbors", $format);
}
- // OSPFv3
- public function ospfv3overviewAction()
+
+ public function ospfoverviewAction($format = "json"): array
{
- return $this->get_ospf3_information('overview');
+ return $this->getInformation("ospf", "overview", $format);
}
- public function ospfv3neighborAction()
+
+ public function ospfneighborAction($format = "json"): array
{
- return $this->get_ospf3_information('neighbor');
+ return $this->getInformation("ospf", "neighbor", $format);
}
- public function ospfv3routeAction()
+
+ public function ospfrouteAction($format = "json"): array
{
- return $this->get_ospf3_information('route');
+ return $this->getInformation("ospf", "route", $format);
}
- public function ospfv3databaseAction()
+
+ public function ospfdatabaseAction($format = "json"): array
{
- return $this->get_ospf3_information('database');
+ return $this->getInformation("ospf", "database", $format);
}
- public function ospfv3interfaceAction()
+
+ public function ospfinterfaceAction($format = "json"): array
{
- return $this->get_ospf3_information('interface');
+ return $this->getInformation("ospf", "interface", $format);
}
- // General
- private function get_general_information($name)
+
+ public function ospfv3overviewAction($format = "json"): array
{
- $backend = new Backend();
- return array("response" => json_decode(trim($backend->configdRun("quagga general-$name")), true));
+ return $this->getInformation("ospfv3", "overview", $format);
}
- public function generalroutesAction()
+
+ public function ospfv3neighborAction($format = "json"): array
{
- return $this->get_general_information('routes');
+ return $this->getInformation("ospfv3", "neighbor", $format);
}
- public function generalroutes6Action()
+
+ public function ospfv3routeAction($format = "json"): array
{
- return $this->get_general_information('routes6');
+ return $this->getInformation("ospfv3", "route", $format);
+ }
+
+ public function ospfv3databaseAction($format = "json"): array
+ {
+ return $this->getInformation("ospfv3", "database", $format);
+ }
+
+ public function ospfv3interfaceAction($format = "json"): array
+ {
+ return $this->getInformation("ospfv3", "interface", $format);
}
}
diff --git a/net/frr/src/opnsense/mvc/app/models/OPNsense/Quagga/Menu/Menu.xml b/net/frr/src/opnsense/mvc/app/models/OPNsense/Quagga/Menu/Menu.xml
index 03dca32de..e294cc115 100644
--- a/net/frr/src/opnsense/mvc/app/models/OPNsense/Quagga/Menu/Menu.xml
+++ b/net/frr/src/opnsense/mvc/app/models/OPNsense/Quagga/Menu/Menu.xml
@@ -5,12 +5,12 @@
-
+
-
+
diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt
index 06818b0cd..60c7d72f1 100644
--- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt
+++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt
@@ -32,82 +32,114 @@ POSSIBILITY OF SUCH DAMAGE.
{{ partial("layout_partials/base_form",['fields':diagnosticsForm,'id':'frm_diagnostics_settings'])}}
#}
-
-
-
+
-
- {{ lang._('loading...') }}
-
-
+
+
+
+
diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt
index 4e15ea266..16c147019 100644
--- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt
+++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt
@@ -28,106 +28,115 @@ POSSIBILITY OF SUCH DAMAGE.
#}
-
diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt
index 0ac926d06..5b446b533 100644
--- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt
+++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt
@@ -28,131 +28,157 @@ POSSIBILITY OF SUCH DAMAGE.
#}
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt
index 65c9d3866..4f4662dbb 100644
--- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt
+++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt
@@ -130,7 +130,7 @@ POSSIBILITY OF SUCH DAMAGE.
{{ lang._('LS ID') }} |
{{ lang._('Advertising Router') }} |
{{ lang._('Age') }} |
- {{ lang._('Sequence Number') }} |
+ {{ lang._('Sequence Number') }} |
{{ lang._('Payload') }} |
@@ -163,7 +163,7 @@ POSSIBILITY OF SUCH DAMAGE.
{{ lang._('LS ID') }} |
{{ lang._('Advertising Router') }} |
{{ lang._('Age') }} |
- {{ lang._('Sequence Number') }} |
+ {{ lang._('Sequence Number') }} |
{{ lang._('Payload') }} |
@@ -192,7 +192,7 @@ POSSIBILITY OF SUCH DAMAGE.
{{ lang._('LS ID') }} |
{{ lang._('Advertising Router') }} |
{{ lang._('Age') }} |
- {{ lang._('Sequence Number') }} |
+ {{ lang._('Sequence Number') }} |
{{ lang._('Payload') }} |
diff --git a/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py b/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py
new file mode 100644
index 000000000..73cb70d5e
--- /dev/null
+++ b/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py
@@ -0,0 +1,445 @@
+#!/usr/local/bin/python3
+"""
+ Copyright (c) 2020 Marc Leuser
+ Copyright (c) 2020 Ad Schellevis
+ 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.
+
+"""
+
+import argparse
+import ujson
+import re
+from typing import List, Dict
+from lib import VtySH
+
+
+class Re:
+ """ Custom regex helper class (source https://stackoverflow.com/a/4980181)
+ Use to conveniently build switch-case like constructs using if/elif
+ """
+
+ def __init__(self):
+ self.last_match = None
+
+ def match(self, pattern, text):
+ self.last_match = re.match(pattern, text)
+ return self.last_match
+
+ def search(self, pattern, text):
+ self.last_match = re.search(pattern, text)
+ return self.last_match
+
+
+class FRRTableReader:
+ def __init__(self, titles: List[str] = None):
+ self.titles = titles if titles is not None else list()
+ self.columns = list()
+
+ def read_header(self, line: str, start_without_title: bool = False, start_without_title_name: str = 'status'):
+ # we're going to create a list of columns. each entry contains title, start index and end index for easy parsing
+ self.columns = []
+
+ # the first column's title may sometimes be empty in FRR's output. we'll have to give it a name though
+ if start_without_title:
+ self.columns.append({
+ 'title': start_without_title_name,
+ 'start_index': 0,
+ 'end_index': None
+ })
+
+ # fill the list with subsequent columns
+ for title in self.titles:
+ try:
+ # find the start index of the current title in the header
+ start_index = line.index(title)
+ except ValueError:
+ # just skip the column if it can't be found
+ continue
+
+ # last column's end index is this column's start index
+ if self.columns:
+ self.columns[-1]['end_index'] = start_index
+
+ # add the current column to the list
+ self.columns.append({
+ 'title': title,
+ 'start_index': start_index,
+ 'end_index': None
+ })
+
+ def read_line(self, line: str) -> Dict[str, str]:
+ result = {}
+
+ # sanity check: the line has to be long enough to contain all columns,
+ # so its length must be greater than the last column's start index
+ if len(line) > self.columns[-1]['start_index']:
+ for column in self.columns:
+ # use the column's name as dict key and just extract the data from start to end index
+ result[column['title'].strip()] = line[column['start_index']:column['end_index']].strip()
+
+ return result
+
+
+class DaemonError(Exception):
+ pass
+
+
+class Daemon:
+ def __init__(self, vtysh: VtySH):
+ self.vtysh = vtysh
+ self.myre = Re()
+
+ def _show(self, suffix: str):
+ # execute the command and filter out empty lines so subsequent iterations don't have to deal with them
+ return list(filter(None, self.vtysh.execute(command='show ' + suffix, translate=bytes.decode).split('\n')))
+
+
+class OSPF(Daemon):
+ def _show(self, suffix: str):
+ return super()._show('ip ospf ' + suffix)
+
+ def database(self):
+ db = {}
+ # table reader for Route Link States
+ rltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum', 'Link count'])
+ # table reader for Net Link States
+ nltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum'])
+ # table reader for Summary Link States
+ sltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum', 'Route\n'])
+ # table reader for AS External Link States (columns are identical to Summary Link States)
+ eltr = sltr
+
+ # get the FRR output
+ lines = self._show('database')
+
+ # placeholder for the active table reader for the coming line
+ tr = None
+ # whether or not the header for the current section has already been parsed
+ header_parsed = False
+ # the current router
+ router = None
+ # the current area
+ area = None
+ # the current mode
+ mode = None
+
+ for line in lines:
+ if line.startswith(' '):
+ # this is a heading
+ heading = line.strip()
+ header_parsed = False
+
+ # this is going to be dirty
+ if self.myre.search(r'OSPF Router with ID \(([\.\d]+)\)', heading):
+ router = self.myre.last_match.group(1)
+ if router not in db:
+ db[router] = {}
+ mode = 'router'
+ elif self.myre.search(r'Router Link States \(Area ([\.\d]+)\)', heading):
+ mode = 'router_link_state_area'
+ area = self.myre.last_match.group(1)
+ if mode not in db[router]:
+ db[router][mode] = {}
+ if area not in db[router][mode]:
+ db[router][mode][area] = []
+ tr = rltr
+ elif self.myre.search(r'Net Link States \(Area ([\.\d]+)\)', heading):
+ mode = 'net_link_state_area'
+ area = self.myre.last_match.group(1)
+ if mode not in db[router]:
+ db[router][mode] = {}
+ if area not in db[router][mode]:
+ db[router][mode][area] = []
+ tr = nltr
+ elif self.myre.search(r'Summary Link States \(Area ([\.\d]+)\)', heading):
+ mode = 'summary_link_state_area'
+ area = self.myre.last_match.group(1)
+ if mode not in db[router]:
+ db[router][mode] = {}
+ if area not in db[router][mode]:
+ db[router][mode][area] = []
+ tr = sltr
+ elif heading == 'AS External Link States':
+ mode = 'external_states'
+ if mode not in db[router]:
+ db[router][mode] = []
+ tr = eltr
+ else:
+ raise DaemonError('failed to parse heading: ' + heading)
+ # told you.
+ else:
+ if not header_parsed:
+ if mode in ['summary_link_state_area', 'external_states']:
+ # workaround because "Route" matches on "Router" and breaks the offset parsing logic
+ # stay consistent with the previous script, add a trailing newline
+ line += '\n'
+ tr.read_header(line)
+ header_parsed = True
+ else:
+ if mode == 'router':
+ raise DaemonError(
+ 'attempting to parse a table row but the mode is \'router\'. '
+ 'please debug the FRR output:\n\n\n' + line
+ )
+ elif mode == 'external_states':
+ db[router][mode].append(tr.read_line(line))
+ else:
+ db[router][mode][area].append(tr.read_line(line))
+
+ return db
+
+
+class OSPFv3(Daemon):
+ def _show(self, suffix: str):
+ return super()._show('ipv6 ospf6 ' + suffix)
+
+ def database(self):
+ database = {}
+ lines = map(str.strip, self._show('database'))
+
+ # table reader
+ tr = FRRTableReader(titles=["Type", "LSId", "AdvRouter", " Age", " SeqNum", " Payload"])
+ header_parsed = False
+ interface = None
+ area = None
+ mode = None
+
+ for line in lines:
+ if self.myre.search(r'Area Scoped Link State Database \(Area (.*)\)', line):
+ header_parsed = False
+ mode = 'scoped_link_db'
+ area = self.myre.last_match.group(1)
+ if mode not in database:
+ database[mode] = {}
+ if area not in database[mode]:
+ database[mode][area] = []
+ elif self.myre.search(r'I\/F Scoped Link State Database \(I\/F (\S+) in Area (.*)\)', line):
+ header_parsed = False
+ mode = 'if_scoped_link_state'
+ interface = self.myre.last_match.group(1)
+ area = self.myre.last_match.group(2)
+ if mode not in database:
+ database[mode] = {}
+ if interface not in database[mode]:
+ database[mode][interface] = {}
+ if area not in database[mode][interface]:
+ database[mode][interface][area] = []
+ elif line == 'AS Scoped Link State Database':
+ header_parsed = False
+ mode = 'as_scoped'
+ if mode not in database:
+ database[mode] = []
+ else:
+ if not header_parsed:
+ tr.read_header(line)
+ header_parsed = True
+ else:
+ if mode == 'scoped_link_db':
+ database[mode][area].append(tr.read_line(line))
+ elif mode == 'if_scoped_link_state':
+ database[mode][interface][area].append(tr.read_line(line))
+ elif mode == 'as_scoped':
+ database[mode].append(tr.read_line(line))
+ else:
+ raise DaemonError('invalid mode, failed to parse line: ' + line)
+
+ return database
+
+ def route(self):
+ route = []
+ lines = map(str.strip, self._show('route'))
+
+ for line in lines:
+ columns = re.split(r'\s+', line)
+ route.append({
+ 'f1': columns[0],
+ 'f2': columns[1],
+ 'network': columns[2],
+ 'gateway': columns[3],
+ 'interface': columns[4],
+ 'time': columns[5],
+ })
+
+ return route
+
+ def interface(self):
+ interface = {}
+ lines = map(str.strip, self._show('interface'))
+
+ current_if = None
+ for line in lines:
+ if self.myre.search(r'(\S+) is (down|up), type ([A-Z]+)', line):
+ current_if = self.myre.last_match.group(1)
+ interface[current_if] = {
+ 'up': True if self.myre.last_match.group(2) == 'up' else False,
+ 'type': self.myre.last_match.group(3),
+ 'enabled': True
+ }
+ elif self.myre.search(r'Interface ID: (\d+)', line):
+ interface[current_if]['id'] = self.myre.last_match.group(1)
+ elif self.myre.search(r'OSPF not enabled on this interface', line):
+ interface[current_if]['enabled'] = False
+ elif self.myre.search(r'Instance ID (\d+), Interface MTU (\d+) \(autodetect: (\d+)\)', line):
+ interface[current_if]['instance_id'] = int(self.myre.last_match.group(1))
+ interface[current_if]['interface_mtu'] = int(self.myre.last_match.group(2))
+ interface[current_if]['interface_mtu_autodetect'] = int(self.myre.last_match.group(3))
+ elif self.myre.search(r'(inet |inet6): (\S+)', line):
+ family = 'IPv6' if self.myre.last_match.group(1) == 'inet6' else 'IPv4'
+ if family not in interface[current_if]:
+ interface[current_if][family] = []
+ interface[current_if][family].append(self.myre.last_match.group(2))
+ elif self.myre.search(r'MTU mismatch detection: (en|dis)abled', line):
+ interface[current_if]['mtu_mismatch_detection'] = True if self.myre.last_match.group(
+ 1) == 'en' else False
+ elif self.myre.search(r'DR: (\S+) BDR: (\S+)', line):
+ interface[current_if]['designated_router'] = self.myre.last_match.group(1)
+ interface[current_if]['backup_designated_router'] = self.myre.last_match.group(2)
+ elif self.myre.search(r'State (\S+), Transmit Delay (\d+) sec, Priority (\d+)', line):
+ interface[current_if]['state'] = self.myre.last_match.group(1)
+ interface[current_if]['transmit_delay'] = int(self.myre.last_match.group(2))
+ interface[current_if]['priority'] = int(self.myre.last_match.group(3))
+ elif self.myre.search(r'Number of I\/F scoped LSAs is (\d+)', line):
+ interface[current_if]['number_if_scoped_lsas'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'(\d+) Pending LSAs for (\S+) in Time ([\d:]+)(?: (.*))', line):
+ if 'pending_lsas' not in interface[current_if]:
+ interface[current_if]['pending_lsas'] = {}
+ interface[current_if]['pending_lsas'][self.myre.last_match.group(2)] = {
+ 'time': self.myre.last_match.group(3),
+ 'count': self.myre.last_match.group(1),
+ 'flags': self.myre.last_match.group(4)
+ }
+ elif self.myre.search(r'Hello (\d+), Dead (\d+), Retransmit (\d+)', line):
+ interface[current_if]['timers'] = {
+ 'hello': int(self.myre.last_match.group(1)),
+ 'dead': int(self.myre.last_match.group(2)),
+ 'retransmit': int(self.myre.last_match.group(3))
+ }
+ elif self.myre.search(r'Area ID (\S+), Cost (\d+)', line):
+ if 'area_cost' not in interface[current_if]:
+ interface[current_if]['area_cost'] = []
+ interface[current_if]['area_cost'].append({
+ 'area': self.myre.last_match.group(1),
+ 'cost': int(self.myre.last_match.group(2))
+ })
+ elif line in ['Internet Address:', 'Timer intervals configured:']:
+ # ignore these strings
+ pass
+ else:
+ raise DaemonError('failed to parse line: ' + line)
+
+ return interface
+
+ def neighbor(self):
+ neighbors = []
+ lines = self._show('neighbor')
+
+ tr = FRRTableReader(titles=['Neighbor ID', 'Pri', 'DeadTime', 'State/IfState', 'Duration I/F[State]'])
+ ll = lines.pop(0)
+ tr.read_header(ll)
+
+ for line in lines:
+ neighbor = tr.read_line(line)
+ neighbor['Pri'] = int(neighbor['Pri'])
+ neighbors.append(neighbor)
+
+ return neighbors
+
+ def overview(self):
+ overview = {'areas': {}}
+ lines = map(str.strip, self._show(''))
+
+ current_area = None
+
+ for line in lines:
+ if self.myre.search(r'OSPFv3 Routing Process \((\d+)\) with Router-ID ([\d\.]+)', line):
+ overview['router_id'] = self.myre.last_match.group(2)
+ overview['routing_process'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'Initial SPF scheduling delay (\d+) millisec\(s\)', line):
+ overview['initial_spf_scheduling_delay'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'(Min|Max)imum hold time between consecutive SPFs (\d+) milli?second\(s\)', line):
+ if 'hold_time' not in overview:
+ overview['hold_time'] = {}
+ overview['hold_time'][self.myre.last_match.group(1).lower()] = int(self.myre.last_match.group(2))
+ elif line == 'This router is an ASBR (injecting external routing information)':
+ overview['asbr'] = True
+ elif self.myre.search(r'SPF timer is (.*)', line):
+ overview['spf_timer'] = self.myre.last_match.group(1)
+ elif self.myre.search(r'Running (.*)', line):
+ overview['running_time'] = self.myre.last_match.group(1)
+ elif self.myre.search(r'Number of AS scoped LSAs is (\d+)', line):
+ overview['number_as_scoped'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'Hold time multiplier is currently (\d+)', line):
+ overview['current_hold_time_multipier'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'Number of areas in this router is (\d+)', line):
+ overview['number_of_areas'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'^Area ([\d\.]*)', line):
+ current_area = self.myre.last_match.group(1)
+ overview['areas'][current_area] = {}
+ elif self.myre.search(r'Interface attached to this area: (.*)', line):
+ overview['areas'][current_area]['interfaces'] = self.myre.last_match.group(1).split(' ')
+ elif self.myre.search(r'Number of Area scoped LSAs is (.*)', line):
+ overview['areas'][current_area]['number_lsas'] = int(self.myre.last_match.group(1))
+ elif self.myre.search(r'LSA minimum arrival (.*)', line) or \
+ 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):
+ # skip these lines
+ pass
+ else:
+ raise DaemonError('failed to parse line: ' + line)
+
+ return overview
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Python port of the OPNsense FRR output parser.')
+ parser.add_argument('-d', '--ospf-database', help='Prints the OSPF Database', action='store_true')
+ parser.add_argument('-D', '--ospfv3-database', help='Prints the OSPFv3 Database', action='store_true')
+ parser.add_argument('-t', '--ospfv3-route', help='Prints the OSPFv3 routing table', action='store_true')
+ parser.add_argument('-I', '--ospfv3-interface', help='Prints OSPFv3 interface information', action='store_true')
+ parser.add_argument('-N', '--ospfv3-neighbor', help='Prints OSPFv3 neighbor information', action='store_true')
+ parser.add_argument('-O', '--ospfv3-overview', help='Prints an OSPFv3 Summary', action='store_true')
+ args = parser.parse_args()
+
+ # initialize VtySH and parser objects
+ main_vtysh = VtySH()
+ ospf = OSPF(main_vtysh)
+ ospfv3 = OSPFv3(main_vtysh)
+
+ main_result = {}
+ if args.ospf_database:
+ main_result['ospf_database'] = ospf.database()
+ elif args.ospfv3_database:
+ main_result['ospfv3_database'] = ospfv3.database()
+ elif args.ospfv3_route:
+ main_result['ospfv3_route'] = ospfv3.route()
+ elif args.ospfv3_interface:
+ main_result['ospfv3_interface'] = ospfv3.interface()
+ elif args.ospfv3_neighbor:
+ main_result['ospfv3_neighbors'] = ospfv3.neighbor()
+ elif args.ospfv3_overview:
+ main_result['ospfv3_overview'] = ospfv3.overview()
+
+ print(ujson.dumps(main_result, escape_forward_slashes=False))
diff --git a/net/frr/src/opnsense/scripts/quagga/diag-bgp.sh b/net/frr/src/opnsense/scripts/quagga/diag-bgp.sh
deleted file mode 100755
index 4d6e81f26..000000000
--- a/net/frr/src/opnsense/scripts/quagga/diag-bgp.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/sh
-
-case "$1" in
- bgp)
- vtysh -d bgpd -c "show ip bgp"
- ;;
- summary)
- vtysh -d bgpd -c "show bgp summary"
- ;;
- neighbor)
- vtysh -d bgpd -c "show ip bgp neighbors $2"
- ;;
- neighbor-adv)
- vtysh -d bgpd -c "show ip bgp neighbors $2 advertised-routes"
- ;;
- *)
- echo "Usage: $0 bgp|summary|neighbor |neighbor-adv "
- exit 1
-esac
-exit 0
diff --git a/net/frr/src/opnsense/scripts/quagga/quagga.rb b/net/frr/src/opnsense/scripts/quagga/quagga.rb
deleted file mode 100755
index a59427855..000000000
--- a/net/frr/src/opnsense/scripts/quagga/quagga.rb
+++ /dev/null
@@ -1,757 +0,0 @@
-#!/usr/local/bin/ruby
-=begin
-Copyright 2017 Fabian Franz
-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 BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
-=end
-
-require 'json'
-require 'shellwords'
-require 'pp'
-
-$QUAGGA_DEBUG = false
-
-class VTYSH
- def initialize(path = '/usr/local/bin/vtysh')
- @path = path
- end
-
- def execute(param)
- o = `#{@path} -c #{param.shellescape}`
- raise "error" if o.length <= 2
- raise "command error - command: #{param}" if o.include? "% Unknown command"
- o
- end
-
- #def execute(param)
- # fn = param.sub("show","sh").gsub(" ","_")
- # File.read(fn)
- #end
-end
-
-class QuaggaTableReader
- attr_accessor :headers
- def initialize(headers = [])
- @headers = headers
- end
- def read_headline(line, start_without_header = false, start_without_header_name = 'status')
- # get begin of header (number of the first char of the string)
- header = line
- header_offset = {}
- header_offset[0] = start_without_header_name if start_without_header
- @headers.map do |x|
- header_offset[header.index(x)] = x.strip
- end
-
-
- # make ranges: this will make a range of the first char of the sting until
- # the the char befor the next heading begins
- ranges = []
- 0.upto (header_offset.keys.length - 2) do |i|
- ranges << ((header_offset.keys[i])...(header_offset.keys[i + 1]))
- end
- # the last one has no next heading - this will go to the end of the line
- ranges.push ((header_offset.keys.last)..-1) # path
- @header_offset = header_offset
- @ranges = ranges
- nil
- end
-
- def read_entry(line, expand_fields = {})
- raise "heading missing" unless @ranges
- tmp = {}
- return tmp unless line&.strip.length > 2
-
- @ranges.each do |r|
- # the string starts here
- b = r.begin
- # get the heading starting where the string starts
- n = @header_offset[b]
- # get the data or return an empty string
- tmp[n] = line[r]&.strip || ""
- end
- # replace characters by the meaning
- expand_fields.keys.each do |key|
- tmp[key] = tmp[key].split("").map {|x| {dn: expand_fields[key][x], abb: x} } if tmp[key]
- end
- tmp
- end
-end
-
-class General
- def initialize(vtysh)
- @vtysh = vtysh
- end
- def routes(ipv6 = false)
- lines = @vtysh.execute("show ip#{ipv6 ? 'v6' : ''} route").lines
-
- # headers
- meanings = {}
- while (line = lines.shift.strip) != ''
- line = line.gsub('Codes: ','')
- line.split(",").each do |meaning|
- short, long = meaning.strip.split(" - ")
- meanings[short] = long
- end
- end
-
- # you don't have to understand this regex ;)
- entry_regex = /(\S+?)\s+?(\S+?)(?: \[(\d+)\/(\d+)\])? (?:(?:via (\S+?)|is ([^,]+?)|), ([^,\n]+)|(unreachable \(blackhole\)))(?:, (\S+))?/
- entries = []
- while (line = lines.shift&.strip)
- if line.length > 10
- code, network, ad, metric, via, direct, interface, unreachable, time = line.scan(entry_regex).first
- code = code.split('').map {|c| {short: c, long: meanings[c]}}
- entries << {code: code, network: (network || direct), ad: ad, via: via || unreachable, metric: metric, interface: interface, time: time }
- end
- end
- entries
- end
-
- def routes6
- routes(true)
- end
-
-end
-
-class OSPF
- def initialize(vtysh)
- @vtysh = vtysh
- end
- def neighbors
- qta = QuaggaTableReader.new(["Neighbor ID", "Pri", "State", "Dead Time", "Address", "Interface", "RXmtL", "RqstL", "DBsmL"])
- lines = @vtysh.execute("show ip ospf neighbor").lines
- lines.shift # empty line
- data = []
- qta.read_headline(lines.shift)
- while (line = lines.shift) && (line.length > 2)
- data << qta.read_entry(line)
- end
- data
- end
-
- def interface
- lines = @vtysh.execute("show ip ospf interface").lines
- interfaces = {}
- current_if = ''
- while line = lines.shift
- next if line.strip.length <= 1
- if line[0] != ' ' # we are in a heading
- current_if = line.split(" ").first
- interfaces[current_if] = {}
- current_if = interfaces[current_if]
- current_if[:enabled] = true
- lines.shift
- else
- line.strip!
- case line
- when 'OSPF not enabled on this interface'
- current_if[:enabled] = false
- when /Internet Address ([^,]+?), Broadcast ([^,]+?), Area (.*)/
- current_if[:address] = $1
- current_if[:broadcast] = $2
- current_if[:area] = $3
- when /MTU mismatch detection:(.*)/
- current_if[:mtu_mismatch_detection] = ($1 == 'enabled')
- when /Router ID ([^,]+?), Network Type ([^,]+?), Cost: (\d+)/
- current_if[:router_id] = $1
- current_if[:network_type] = $2
- current_if[:cost] = $3.to_i
- when /Transmit Delay is (\d+) sec, State ([^,]+?), Priority (\d+)/
- current_if[:transmit_delay] = $1.to_i
- current_if[:state] = $2
- current_if[:priority] = $3.to_i
- when "No designated router on this network"
- current_if[:designated_router] = nil
- when /Designated Router \(ID\) ([^,]+?), Interface Address (.*)/
- current_if[:designated_router] = $1
- current_if[:designated_router_interface_address] = $2
- when "No backup designated router on this network"
- current_if[:backup_designated_router] = nil
- when /Timer intervals configured, Hello (\d+)s, Dead (\d+)s, Wait (\d+)s, Retransmit (\d+)/
- current_if[:intervals] = {hello: $1.to_i, dead: $2.to_i, wait: $3.to_i, retransmit: $4.to_i}
- when /Multicast group memberships: (.*)/
- current_if[:multicast_group_memberships] = $1.split(" ")
- when /Hello due in ([\d\.]+|inactive)s?/
- current_if[:hello_due_in] = $1 == 'inactive' ? $1 : $1.to_f
- when /Neighbor Count is (\d+), Adjacent neighbor count is (\d+)/
- current_if[:neighbor_count] = $1.to_i
- current_if[:adjacent_neighbor_count] = $2.to_i
- else
- # make sure there is an array to write in
- current_if[:unparsed] ||= []
- current_if[:unparsed] << line
- puts line if $QUAGGA_DEBUG
- end
- end
- end
- interfaces
- end
-
- def database
- lines = @vtysh.execute("show ip ospf database").lines
- db = {}
- heading = ''
- router = ''
- router_link_states_area = ''
- mode = :none
- qta = nil
- while line = lines.shift
- next if line == ''
- if line[0] == ' ' # heading
- heading = line.strip
- case heading
- when /OSPF Router with ID \(([\.\d]+)\)/
- router = $1
- db[router] ||= {}
- mode = :router
- when /Router Link States \(Area ([\.\d]+)\)/
- router_link_states_area = $1
- db[router]['router_link_state_area'] ||= {}
- db[router]['router_link_state_area'][$1] ||= []
- mode = :router_link_state
- qta = nil
- when /Net Link States \(Area ([\.\d]+)\)/
- net_link_states_area = $1
- db[router]['net_link_state_area'] ||= {}
- db[router]['net_link_state_area'][$1] ||= []
- mode = :net_link_state
- qta = nil
- when 'AS External Link States'
- mode = :states
- db[router]['external_states'] ||= []
- qta = nil
- else
- puts "unknown heading" if $QUAGGA_DEBUG
- end
- else
- if qta == nil
- case mode
- when :router_link_state
- qta = QuaggaTableReader.new(["Link ID", "ADV Router", "Age", "Seq#", "CkSum", "Link count"])
- when :net_link_state
- qta = QuaggaTableReader.new(["Link ID", "ADV Router", "Age", "Seq#", "CkSum"])
- when :states
- qta = QuaggaTableReader.new(["Link ID", "ADV Router", "Age", "Seq#", "CkSum", "Route\n"])
- else
- next
- end
- headline = lines.shift
- qta.read_headline(headline,true)
- else
- entry = qta.read_entry(line)
- case mode
- when :router_link_state
- db[router]['router_link_state_area'][router_link_states_area] << entry
- when :net_link_state
- db[router]['net_link_state_area'][net_link_states_area] << entry
- when :states
- db[router]['external_states'] << entry
- end
- end
- end
- # table
- end
- db
- end
-
- def route
- lines = @vtysh.execute("show ip ospf route").lines
- heading = ''
- route = {}
- last_line = []
- while line = lines.shift
- if line[0] == "=" #heading
- heading = line.scan(/=* ([^=]*) =*/).first.first
- route[heading] = []
- else # data
- case line.strip
- when /N\s+([\d\.\/]+)\s+\[(\d+)\]\s+area:\s(.*)/
- last_line = {network: $1, cost: $2.to_i, area: $3, type: 'N'}
- route[heading] << last_line
- when /N (E(?:\d+) (?:\S+))\s+\[([\d\/]+)\] tag: (\d+)/
- last_line = {network: $1, cost: $2, tag: $3.to_i, type: 'N'}
- route[heading] << last_line
- when /(?:(directly attached) to|via ([^,]+),) (.*)/
- last_line[:via] = $1 || $2
- last_line[:via_interface] = $3
- when /R\s+(\S+)\s+\[(\d+)\] area: ([^,]+)(, ASBR)/
- last_line = {ip: $1, cost: $2.to_i, area: $3, asbr: (", ASBR" == $4), type: 'R'}
- route[heading] << last_line
- else
- puts line if $QUAGGA_DEBUG
- end
- end
- end
- route
- end
-
- def overview
- lines = @vtysh.execute("show ip ospf").lines
- overview = {rfc2328_conform: false, asbr: false}
- while line = lines.shift&.strip
- case line
- when /OSPF Routing Process, Router ID: ([\d\.]+)/
- overview[:router_id] = $1
- when "This implementation conforms to RFC2328"
- overview[:rfc2328_conform] = true
- when /OpaqueCapability flag is (\S+)/
- overview[:opaque_capability] = ($1 == 'enabled')
- when /Initial SPF scheduling delay (\d+) millisec\(s\)/
- overview[:initial_spf_scheduling_delay] = $1.to_i
- when /(Min|Max)imum hold time between consecutive SPFs (\d+) millisec\(s\)/
- overview[:hold_time] ||= {}
- overview[:hold_time][$1.downcase] = $2.to_i
- when "This router is an ASBR (injecting external routing information)"
- overview[:asbr] = true
- when /Number of external LSA (\d+). Checksum Sum ([x\d]+)/
- overview[:external_lsa] = {count: $1.to_i, checksum: $2}
- when /Number of opaque AS LSA (\d+). Checksum Sum ([x\d]+)/
- overview[:opaque_as_lsa] = {count: $1.to_i, checksum: $2}
- when /Refresh timer (\d+) secs/
- overview[:refresh_timer] = $1.to_i
- when /Number of areas attached to this router: (\d+)/
- overview[:areas_attached_count] = $1.to_i
- when /Hold time multiplier is currently (\d+)/
- overview[:current_hold_time_multipier] = $1.to_i
- when /RFC1583Compatibility flag is (\S+)/
- overview[:rfc1583_compatibility] = ($1 == 'enabled')
- when /SPF timer is (.*)/
- overview[:spf_timer] = $1
- when ""
- break
- else
- puts line if $QUAGGA_DEBUG
- end
- end
- # general overview has ended - now the area overviews come
- overview[:areas] = {}
- current_area = {}
- while line = lines.shift&.strip
- case line
- when /Area ID: (.*)/
- current_area = {}
- overview[:areas][$1] = current_area
- when /Number of interfaces in this area: Total: (\d+), Active: (\d+)/
- current_area[:interfaces] = {total: $1.to_i,active: $2.to_i}
- when /Number of (router|network|summary) LSA (\d+). Checksum Sum ([\da-fx]+)/
- current_area[:lsa] ||= {}
- current_area[:lsa][$1] = {count: $2.to_i, checksum: $3}
- when /Number of LSA (\d+)/
- current_area[:lsa] ||= {}
- current_area[:lsa][:count] = $1.to_i
- when /Number of (opaque (?:area|link)|NSSA|ASBR summary) LSA (\d+). Checksum Sum ([\da-fx]+)/
- current_area[:lsa] ||= {}
- current_area[:lsa][$1] = {count: $2.to_i, checksum: $3}
- when /Number of fully adjacent neighbors in this area: (\d+)/
- current_area[:fully_adjacent_neighbor_count] = $1.to_i
- when /SPF algorithm executed (\d) times/
- current_area[:spf_exec_count] = $1.to_i
- when "Area has no authentication"
- current_area[:auth] = "none"
- else
- puts line if $QUAGGA_DEBUG
- end
- end
- overview
- end
-end
-
-class BGP
- def initialize(sh)
- @vtysh = sh
- end
-
- def overview
- output = @vtysh.execute('show ip bgp')
- return {} if output.include? "No BGP process is configured"
- return {} unless output.include? 'version' # we get an empty output if quagga is not running
- output = output.split("\n")
- bgp = {}
-
- # Process the header/definitions
- while line = output.shift&.rstrip
- case line
- when /^BGP table version/
- x,y = line.scan(/.*?version is (\d+).*?ID is ([0-9\.]+).*/).first
- bgp['table_version'] = x
- bgp['local_router_id'] = y
- when /^Status codes/
- # find out, what the status abbreviations mean
- status_codes = {}
- line.split(":").last.strip.split(",").each do |x|
- k,v = x.strip.split(" ")
- status_codes[k] = v
- end
- while line.end_with? ","
- line = output.shift
- line.strip.split(",").each do |x|
- k,v = x.strip.split(" ")
- status_codes[k] = v
- end
- end
- when /^Origin codes/
- # same like before but for the origin codes
- origin_codes = {}
- line.split(":").last.strip.split(",").each do |x|
- k,v = x.strip.split(" - ")
- origin_codes[k] = v
- end
- when /^Nexthop codes/
- # Just eat this line, nothing to do with it
- when ""
- # Found the end of the header, reached the table
- break
- else
- # eat all other (unexpected) lines
- puts line if $QUAGGA_DEBUG
- end
- end
-
- # Process the tabular data
- bgp['output'] = []
- qta = QuaggaTableReader.new(["Network", "Next Hop", "Metric", "LocPrf", "Weight", "Path"])
- qta.read_headline(output.shift,true)
- while line = output.shift&.strip
- break if line == ''
- data = qta.read_entry(line)
- data['status'] = data['status'].split("").map {|x| {dn: status_codes[x], abb: x} }
- data['Path'] = data['Path'].split("").map {|x| {dn: origin_codes[x], abb: x} }
- bgp['output'] << data
- end
- bgp
- end
-end
-
-class OSPFv3
- def initialize(sh)
- @vtysh = sh
- end
-
- def overview
- lines = @vtysh.execute("show ipv6 ospf6").lines
- overview = {}
- while line = lines.shift&.strip
- case line
- when /OSPFv3 Routing Process \((\d+)\) with Router-ID ([\d\.]+)/
- overview[:router_id] = $2
- overview[:routing_process] = $1.to_i
- when /Initial SPF scheduling delay (\d+) millisec\(s\)/
- overview[:initial_spf_scheduling_delay] = $1.to_i
- # this line contains a typo in the output - I made it to work with and without
- # this typo
- when /(Min|Max)imum hold time between consecutive SPFs (\d+) milli?second\(s\)/
- overview[:hold_time] ||= {}
- overview[:hold_time][$1.downcase] = $2.to_i
- when "This router is an ASBR (injecting external routing information)"
- overview[:asbr] = true
- when /SPF timer is (.*)/
- overview[:spf_timer] = $1
- when /Running (.*)/
- overview[:running_time] = $1
- when /Number of AS scoped LSAs is (\d+)/
- overview[:number_as_scoped] = $1.to_i
- when /Hold time multiplier is currently (\d+)/
- overview[:current_hold_time_multipier] = $1.to_i
- when /Number of areas in this router is (\d+)/
- overview[:number_of_areas] = $1.to_i
- when ""
- break
- else
- # debug
- puts line if $QUAGGA_DEBUG
- end
- end
- # general overview has ended - now the area overviews come
- overview[:areas] = {}
- current_area = {}
- while line = lines.shift&.strip
- case line
- when /^Area ([\d\.]*)/
- current_area = {}
- overview[:areas][$1] = current_area
- when /Interface attached to this area: (.*)/
- current_area[:interfaces] = $1.split(" ")
- when /Number of Area scoped LSAs is (.*)/
- current_area[:number_lsas] = $1.to_i
- else
- puts line if $QUAGGA_DEBUG
- end
- end
- overview
- end
-
- def linkstate
- lines = @vtysh.execute("show ipv6 ospf6 linkstate").lines
- linkstate = {}
-
- qta = nil
- current_area = []
- while line = lines.shift&.strip
- case line
- when /SPF Result in Area (.*)/
- linkstate[$1] = current_area = []
- qta = QuaggaTableReader.new(["Type","Router-ID", "Net-ID", "Rtr-Bits", "Options", "Cost"])
- lines.shift
- qta.read_headline(lines.shift)
- else
- if line.length > 10
- current_area << qta.read_entry(line)
- end
- end
- end
- linkstate
- end
-
- def route
- route = []
- lines = @vtysh.execute("show ipv6 ospf6 route").lines
-
- lines.each do |line|
- f1, f2, network, gateway, interface, time = line.strip.split(/\s+/)
- route << { f1: f1,
- f2: f2,
- network: network,
- gateway: gateway,
- interface: interface,
- time: time }
- end
- route
- end
-
- def neighbors
- qta = QuaggaTableReader.new(["Neighbor ID","Pri", "DeadTime", "State/IfState", "Duration I/F[State]"])
- neighbor = []
- nb = @vtysh.execute("show ipv6 ospf6 neighbor").lines
- qta.read_headline(nb.shift.strip)
- while line = nb.shift&.strip
- puts line
- if line.length > 10
- tmp = qta.read_entry(line)
- tmp['Pri'] = tmp['Pri'].to_i
- neighbor << tmp
- end
- end
- neighbor
- end
- def database
- lines = @vtysh.execute("show ipv6 ospf6 database").lines
- database = {}
- mode = :none
- area = ''
- qta = :none
- while line = lines.shift&.strip
- case line
- when /Area Scoped Link State Database \(Area (.*)\)/
- mode = :scoped_link_db
- database[:scoped_link_db] ||= {}
- database[:scoped_link_db][$1] = area = []
- qta = database_qta(lines)
- when /I\/F Scoped Link State Database \(I\/F (\S+) in Area (.*)\)/
- mode = :if_scoped_link_state
- database[:if_scoped_link_state] ||= {}
- database[:if_scoped_link_state][$1] ||= {}
- area = database[:if_scoped_link_state][$1][$2] ||= []
- qta= database_qta(lines)
- when "AS Scoped Link State Database"
- mode = :as_scoped
- area = database[:as_scoped] ||= []
- qta=database_qta(lines)
- # note: i have no data for this but i think it looks like the others
- else
- if line.length > 10
- area << qta.read_entry(line)
- end
- end
- end
- database
- end
-
- def interface
- lines = @vtysh.execute("show ipv6 ospf6 interface").lines
- int = {}
- current_if = {}
- while line = lines.shift
- if line.length > 5
- case line.strip
- when /(\S+) is (down|up), type ([A-Z]+)/
- current_if = int[$1] = {up: ($2 == "up" ? true : false),
- type: $3,
- enabled: true}
- when /Interface ID: (\d+)/
- current_if[:id] = $1.to_i
- when /OSPF not enabled on this interface/
- current_if[:enabled] = false
- when /Instance ID (\d+), Interface MTU (\d+) \(autodetect: (\d+)\)/
- current_if[:instance_id] = $1.to_i
- current_if[:interface_mtu] = $2.to_i
- current_if[:interface_mtu_autodetect] = $3.to_i
- when "Internet Address:"
- # ignore
- when /(inet |inet6): (\S+)/
- current_if[:IPv6] ||= []
- current_if[:IPv4] ||= []
- family = $1 == 'inet6' ? :IPv6 : :IPv4
- address = $2
- current_if[family] << address
- when /MTU mismatch detection: (en|dis)abled/
- current_if[:mtu_mismatch_detection] = $1 == 'en'
- when /DR: (\S+) BDR: (\S+)/
- current_if[:designated_router] = $1
- current_if[:backup_designated_router] = $2
- when /State (\S+), Transmit Delay (\d+) sec, Priority (\d+)/
- current_if[:state] = $1
- current_if[:transmit_delay] = $2.to_i
- current_if[:priority] = $3.to_i
- when /Number of I\/F scoped LSAs is (\d+)/
- current_if[:number_if_scoped_lsas] = $1.to_i
- when /(\d+) Pending LSAs for (\S+) in Time ([\d:]+)(?: (.*))/
- current_if[:pending_lsas] ||= {}
- current_if[:pending_lsas][$2] = {time: $3,
- count: $1,
- flags: $4}
- when "Timer intervals configured:"
- # ignore
- when /Hello (\d+), Dead (\d+), Retransmit (\d+)/
- current_if[:timers] = {hello: $1.to_i,
- dead: $2.to_i,
- retransmit: $3.to_i }
- when /Area ID (\S+), Cost (\d+)/
- current_if[:area_cost] ||= []
- current_if[:area_cost] << {area: $1, cost: $2.to_i }
- else
- puts line if $QUAGGA_DEBUG
- end
- end
- end
- int
- end
-
- private
- def database_qta(lines)
- # DON'T REMOVE THE SPACES!!!
- # For some reasons the fields are right aligned with the fields which makes it hard
- # to parse. I don't know a better way to get the correct offset except automatically.
- # (Detection of semantic of the fields)
- qta = QuaggaTableReader.new(["Type", "LSId", "AdvRouter", " Age", " SeqNum"," Payload"])
- lines.shift
- qta.read_headline(lines.shift)
- qta
- end
-end
-
-require 'optparse'
-options = {}
-
-OptionParser.new do |opts|
- opts.banner = "Usage: #{__FILE__} -s section [section specific params]"
- #### OSPFv2
- opts.on("-d", "--ospf-database", "Prints the OSPF Database") do |od|
- options[:ospf_database] = od
- end
- opts.on("-r", "--ospf-route", 'print OSPF routing table') do |od|
- options[:ospf_route] = od
- end
- opts.on("-i", "--ospf-interface", 'print OSPF interface information') do |od|
- options[:ospf_interface] = od
- end
- opts.on("-n", "--ospf-neighbor", 'Print OSPF neighbors') do |od|
- options[:ospf_neighbors] = od
- end
- opts.on("-o", "--ospf-overview", "Print OSPF summary") do |od|
- options[:ospf_overview] = od
- end
- #### OSPFv3
- opts.on("-D", "--ospfv3-database", "Prints the OSPFv3 Database") do |od|
- options[:ospfv3_database] = od
- end
- opts.on("-t", "--ospfv3-route", 'print OSPFv3 routing table') do |od|
- options[:ospfv3_route] = od
- end
- opts.on("-I", "--ospfv3-interface", 'print OSPFv3 interface information') do |od|
- options[:ospfv3_interface] = od
- end
- opts.on("-N", "--ospfv3-neighbor", 'Print OSPFv3 neighbors') do |od|
- options[:ospfv3_neighbors] = od
- end
- opts.on("-O", "--ospfv3-overview", "Print OSPFv3 summary") do |od|
- options[:ospfv3_overview] = od
- end
- #### general things about routing
- opts.on("-R", "--general-routes", "Print Routing Table (IPv4)") do |od|
- options[:general_routes] = od
- end
- opts.on("-6", "--general-routes6", "Print Routing Table (IPv6)") do |od|
- options[:general_routes6] = od
- end
- ### BGP
- opts.on("-B", "--bgp-overview", "Print an overview of BGP") do |od|
- options[:bgp_overview] = od
- end
- ### program opts
- opts.on("-H", "--human-readable", "Print the output human readable (not json)") do |od|
- options[:human_readable] = od
- end
- opts.on("-X", "--debug", "Prints debug output") do |od|
- $QUAGGA_DEBUG = true
- end
- opts.on("-h", "--help", "Prints this help") do
- puts opts
- exit
- end
-end.parse!
-# use the lib
-sh = VTYSH.new
-ospf = OSPF.new sh
-ospfv3 = OSPFv3.new sh
-bgp = BGP.new sh
-general = General.new sh
-
-result = {}
-options.keys.each do |k|
- # if it is true
- if options[k]
- begin
- if k.to_s.include? 'ospf_'
- cmd = k.to_s.split('_').last
- result[k] = ospf.send(cmd)
- elsif k.to_s.include? 'ospfv3'
- cmd = k.to_s.split('_').last
- result[k] = ospfv3.send(cmd)
- elsif k.to_s.include? 'general'
- cmd = k.to_s.split('_').last
- result[k] = general.send(cmd)
- elsif k.to_s.include? 'bgp'
- cmd = k.to_s.split('_').last
- result[k] = bgp.send(cmd)
- end
- rescue # do nothing on an error
- result[k] = "error"
- puts $! if $QUAGGA_DEBUG
- end
- end
-end
-
-# ospf.database, general.routes, ospf.interface, ospf.neighbors, ospf.route, ospf.overview
-if options[:human_readable]
- pp result
-else
- print result.to_json
-end
diff --git a/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf b/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf
index 1b3075f56..0544b9ab5 100644
--- a/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf
+++ b/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf
@@ -22,92 +22,260 @@ parameters:
type:script_output
message:request quagga
-[diag-bgp]
-command:/usr/local/opnsense/scripts/quagga/diag-bgp.sh
-parameters:%s
-type:script_output
-message:bgp diagnostics
-
-[diag-bgp2]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --bgp-overview
+[diagnostics.general_running-config]
+command:/usr/local/bin/vtysh -c "show running-config"
parameters:
type:script_output
-message:bgp diagnostics
+message:FRR diagnosticts "show running-config"
-[ospf-database]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-database
+[diagnostics.general_route4]
+command:/usr/local/bin/vtysh -c "show ip route"
parameters:
type:script_output
-message: Shows the OSPF database
+message:FRR diagnosticts "show ip route"
-[ospf-route]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-route
+[diagnostics.general_route4_json]
+command:/usr/local/bin/vtysh -c "show ip route json"
parameters:
type:script_output
-message: print OSPF routing table
+message:FRR diagnosticts "show ip route json"
-[ospf-interface]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-interface
+[diagnostics.general_route6]
+command:/usr/local/bin/vtysh -c "show ipv6 route"
parameters:
type:script_output
-message: print OSPF interface information
+message:FRR diagnosticts "show ipv6 route"
-[ospf-neighbor]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-neighbor
+[diagnostics.general_route6_json]
+command:/usr/local/bin/vtysh -c "show ipv6 route json"
parameters:
type:script_output
-message: Print OSPF neighbors
+message:FRR diagnosticts "show ipv6 route json"
-[ospf-overview]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-overview
+[diagnostics.bgp_route]
+command:exit 1
parameters:
type:script_output
-message: Print OSPF summary
+message:FRR diagnostics "show bgp all" (not implemented)
-[ospfv3-database]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospfv3-database
+[diagnostics.bgp_route_json]
+command:/usr/local/bin/vtysh -c "show bgp all json"
parameters:
type:script_output
-message: Shows the OSPF database
+message:FRR diagnostics "show bgp all json" (not implemented)
-[ospfv3-route]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospfv3-route
+[diagnostics.bgp_route4]
+command:/usr/local/bin/vtysh -c "show bgp ipv4"
parameters:
type:script_output
-message: print OSPF routing table
+message:FRR diagnostics "show bgp ipv4"
-[ospfv3-interface]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospfv3-interface
+[diagnostics.bgp_route4_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv4 json"
parameters:
type:script_output
-message: print OSPF interface information
+message:FRR diagnostics "show bgp ipv4 json"
-[ospfv3-neighbor]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospfv3-neighbor
+[diagnostics.bgp_route6]
+command:/usr/local/bin/vtysh -c "show bgp ipv6"
parameters:
type:script_output
-message: Print OSPF neighbors
+message:FRR diagnostics "show bgp ipv6"
-[ospfv3-overview]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospfv3-overview
+[diagnostics.bgp_route6_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv6 json"
parameters:
type:script_output
-message: Print OSPF summary
+message:FRR diagnostics "show bgp ipv6 json"
-[general-routes]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --general-routes
+[diagnostics.bgp_summary]
+command:/usr/local/bin/vtysh -c "show bgp summary"
parameters:
type:script_output
-message: Print IPv4 Routing Table
+message:FRR diagnostics "show bgp summary"
-[general-routes6]
-command:/usr/local/opnsense/scripts/quagga/quagga.rb --general-routes6
+[diagnostics.bgp_summary_json]
+command:/usr/local/bin/vtysh -c "show bgp summary json"
parameters:
type:script_output
-message: Print IPv6 Routing Table
+message:FRR diagnostics "show bgp summary json"
-[general-runningconfig]
-command:/usr/local/bin/vtysh -c "show run"
+[diagnostics.bgp_summary4]
+command:/usr/local/bin/vtysh -c "show bgp ipv4 summary"
parameters:
type:script_output
-message: Show running configuration
+message:FRR diagnostics "show bgp ipv4 summary"
+
+[diagnostics.bgp_summary4_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv4 summary json"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv4 summary json"
+
+[diagnostics.bgp_summary6]
+command:/usr/local/bin/vtysh -c "show bgp ipv6 summary"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv6 summary"
+
+[diagnostics.bgp_summary6_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv6 summary json"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv6 summary json"
+
+[diagnostics.bgp_neighbors]
+command:/usr/local/bin/vtysh -c "show bgp neighbors"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp neighbors"
+
+[diagnostics.bgp_neighbors_json]
+command:/usr/local/bin/vtysh -c "show bgp neighbors json"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp neighbors json"
+
+[diagnostics.bgp_neighbors4]
+command:/usr/local/bin/vtysh -c "show bgp ipv4 neighbors"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv4 neighbors"
+
+[diagnostics.bgp_neighbors4_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv4 neighbors json"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv4 neighbors json"
+
+[diagnostics.bgp_neighbors6]
+command:/usr/local/bin/vtysh -c "show bgp ipv6 neighbors"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv6 neighbors"
+
+[diagnostics.bgp_neighbors6_json]
+command:/usr/local/bin/vtysh -c "show bgp ipv6 neighbors json"
+parameters:
+type:script_output
+message:FRR diagnostics "show bgp ipv6 neighbors json"
+
+[diagnostics.ospf_overview]
+command:/usr/local/bin/vtysh -c "show ip ospf"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf"
+
+[diagnostics.ospf_overview_json]
+command:/usr/local/bin/vtysh -c "show ip ospf json"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf json"
+
+[diagnostics.ospf_neighbor]
+command:/usr/local/bin/vtysh -c "show ip ospf neighbor"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf neighbor"
+
+[diagnostics.ospf_neighbor_json]
+command:/usr/local/bin/vtysh -c "show ip ospf neighbor json"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf neighbor json"
+
+[diagnostics.ospf_route]
+command:/usr/local/bin/vtysh -c "show ip ospf route"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf route"
+
+[diagnostics.ospf_route_json]
+command:/usr/local/bin/vtysh -c "show ip ospf route json"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf route json"
+
+[diagnostics.ospf_database]
+command:/usr/local/bin/vtysh -c "show ip ospf database"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf database"
+
+[diagnostics.ospf_database_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospf-database
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf database json"
+
+[diagnostics.ospf_interface]
+command:/usr/local/bin/vtysh -c "show ip ospf interface"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf interface"
+
+[diagnostics.ospf_interface_json]
+command:/usr/local/bin/vtysh -c "show ip ospf interface json"
+parameters:
+type:script_output
+message:FRR diagnostics "show ip ospf interface json"
+
+[diagnostics.ospfv3_overview]
+command:/usr/local/bin/vtysh -c "show ipv6 ospf6"
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6"
+
+[diagnostics.ospfv3_overview_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-overview
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 json"
+
+[diagnostics.ospfv3_neighbor]
+command:/usr/local/bin/vtysh -c "show ipv6 ospf6 neighbor"
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 neighbor"
+
+[diagnostics.ospfv3_neighbor_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-neighbor
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 neighbor json"
+
+[diagnostics.ospfv3_route]
+command:/usr/local/bin/vtysh -c "show ipv6 ospf6 route"
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 route"
+
+[diagnostics.ospfv3_route_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-route
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 route json"
+
+[diagnostics.ospfv3_database]
+command:/usr/local/bin/vtysh -c "show ipv6 ospf6 database"
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 database"
+
+[diagnostics.ospfv3_database_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-database
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 database json"
+
+[diagnostics.ospfv3_interface]
+command:/usr/local/bin/vtysh -c "show ipv6 ospf6 interface"
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 interface"
+
+[diagnostics.ospfv3_interface_json]
+command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-interface
+parameters:
+type:script_output
+message:FRR diagnostics "show ipv6 ospf6 interface json"