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"