diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sonic_ax_impl/lib/__init__.py b/src/sonic_ax_impl/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sonic_ax_impl/lib/vtysh_helper.py b/src/sonic_ax_impl/lib/vtysh_helper.py new file mode 100644 index 0000000000..83d3fb9237 --- /dev/null +++ b/src/sonic_ax_impl/lib/vtysh_helper.py @@ -0,0 +1,130 @@ +import re +import ipaddress +import socket + +HOST = '127.0.0.1' +PORT = 2605 + +def union_bgp_sessions(): + bgpsumm_ipv4 = show_bgp_summary('ip') + sessions_ipv4 = parse_bgp_summary(bgpsumm_ipv4) + + bgpsumm_ipv6 = show_bgp_summary('ipv6') + sessions_ipv6 = parse_bgp_summary(bgpsumm_ipv6) + + # Note: sessions_ipv4 will overwrite sessions_ipv6 if key is the same + sessions = {} + for ses in sessions_ipv6 + sessions_ipv4: + nei = ses['Neighbor'] + sessions[nei] = ses + return sessions + +def vtysh_run(command): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((HOST, PORT)) + + cmd = b"zebra\n" + command.encode() + b"\nexit\n" + s.send(cmd) + + acc = b"" + while True: + data = s.recv(1024) + if not data: + break + acc += data + + s.close() + return acc.decode('ascii', 'ignore') + +def show_bgp_summary(ipver): + assert(ipver in ['ip', 'ipv6']) + try: + result = vtysh_run('show %s bgp summary' % ipver) + + except ConnectionRefusedError as e: + raise RuntimeError('Failed to connect quagga socket') from e + except OSError as e: + raise RuntimeError('Socket error when talking with quagga') from e + return result + +def parse_bgp_summary(summ): + ls = summ.splitlines() + bgpinfo = [] + + ## Read until the table header + n = len(ls) + li = 0 + while li < n: + l = ls[li] + if l.startswith('Neighbor '): break + if l.startswith('No IPv'): # eg. No IPv6 neighbor is configured + return bgpinfo + if l.endswith('> exit'): # last command in the lines + return bgpinfo + li += 1 + + ## Read and store the table header + if li >= n: + raise ValueError('No table header found') + hl = ls[li] + li += 1 + ht = re.split('\s+', hl.rstrip()) + hn = len(ht) + + ## Read rows in the table + while li < n: + l = ls[li] + li += 1 + if l == '': break + + ## Handle line wrap + ## ref: bgp_show_summary in https://github.com/Azure/sonic-quagga/blob/debian/0.99.24.1/bgpd/bgp_vty.c + if ' ' not in l: + ## Read next line + if li >= n: + raise ValueError('Unexpected line wrap') + l += ls[li] + li += 1 + + ## Note: State/PfxRcd field may be 'Idle (Admin)' + lt = re.split('\s+', l.rstrip(), maxsplit = hn - 1) + if len(lt) != hn: + raise ValueError('Unexpected row in the table') + dic = dict(zip(ht, lt)) + bgpinfo.append(dic) + return bgpinfo + +STATE_CODE = { + "Idle": 1, + "Idle (Admin)": 1, + "Connect": 2, + "Active": 3, + "OpenSent": 4, + "OpenConfirm": 5, + "Established": 6 +}; + +def bgp_peer_tuple(dic): + nei = dic['Neighbor'] + ver = dic['V'] + sta = dic['State/PfxRcd'] + + # prefix '*' appears if the entry is a dynamic neighbor + nei = nei[1:] if nei[0] == '*' else nei + ip = ipaddress.ip_address(nei) + if type(ip) is ipaddress.IPv4Address: + oid_head = (1, 4) + else: + oid_head = (2, 16) + + oid_ip = tuple(i for i in ip.packed) + + if sta.isdigit(): + status = 6 + elif sta in STATE_CODE: + status = STATE_CODE[sta] + else: + return None, None + + return oid_head + oid_ip, status + diff --git a/src/sonic_ax_impl/main.py b/src/sonic_ax_impl/main.py index 7d176ceb6a..0275900465 100644 --- a/src/sonic_ax_impl/main.py +++ b/src/sonic_ax_impl/main.py @@ -32,6 +32,7 @@ class SonicMIB( ieee802_1ab.LLDPRemTable, dell.force10.SSeriesMIB, cisco.mgmt.CiscoSystemExtMIB, + cisco.bgp4.CiscoBgp4MIB, cisco.ciscoPfcExtMIB.cpfcIfTable, cisco.ciscoPfcExtMIB.cpfcIfPriorityTable, cisco.ciscoSwitchQosMIB.csqIfQosGroupStatsTable, diff --git a/src/sonic_ax_impl/mibs/vendor/cisco/__init__.py b/src/sonic_ax_impl/mibs/vendor/cisco/__init__.py index 822ac73a19..e295a873b0 100644 --- a/src/sonic_ax_impl/mibs/vendor/cisco/__init__.py +++ b/src/sonic_ax_impl/mibs/vendor/cisco/__init__.py @@ -1,4 +1,4 @@ -from . import mgmt +from . import mgmt, bgp4 from . import ciscoPfcExtMIB from . import ciscoSwitchQosMIB from . import ciscoEntityFruControlMIB diff --git a/src/sonic_ax_impl/mibs/vendor/cisco/bgp4.py b/src/sonic_ax_impl/mibs/vendor/cisco/bgp4.py new file mode 100644 index 0000000000..ce03675b66 --- /dev/null +++ b/src/sonic_ax_impl/mibs/vendor/cisco/bgp4.py @@ -0,0 +1,45 @@ +from bisect import bisect_right +from sonic_ax_impl import mibs +from sonic_ax_impl.lib import vtysh_helper +from ax_interface import MIBMeta, ValueType, MIBUpdater, SubtreeMIBEntry +from ax_interface.mib import MIBEntry + +class BgpSessionUpdater(MIBUpdater): + def __init__(self): + super().__init__() + self.session_status_map = {} + self.session_status_list = [] + + def update_data(self): + self.session_status_map = {} + self.session_status_list = [] + + try: + sessions = vtysh_helper.union_bgp_sessions() + except RuntimeError as e: + mibs.logger.error("Failed to union bgp sessions: {}.".format(e)) + return + + for nei, ses in sessions.items(): + oid, status = vtysh_helper.bgp_peer_tuple(ses) + if oid is None: continue + self.session_status_list.append(oid) + self.session_status_map[oid] = status + + self.session_status_list.sort() + + def sessionstatus(self, sub_id): + return self.session_status_map.get(sub_id, None) + + def get_next(self, sub_id): + right = bisect_right(self.session_status_list, sub_id) + if right >= len(self.session_status_list): + return None + + return self.session_status_list[right] + + +class CiscoBgp4MIB(metaclass=MIBMeta, prefix='.1.3.6.1.4.1.9.9.187'): + bgpsession_updater = BgpSessionUpdater() + + cbgpPeer2State = SubtreeMIBEntry('1.2.5.1.3', bgpsession_updater, ValueType.INTEGER, bgpsession_updater.sessionstatus) diff --git a/tests/__init__.py b/tests/__init__.py index 16fb37ac9e..98cfd92200 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,2 @@ +import tests.mock_tables.socket import tests.mock_tables.imp diff --git a/tests/mock_tables/bgpsummary_ipv4.txt b/tests/mock_tables/bgpsummary_ipv4.txt new file mode 100644 index 0000000000..44f8b69da8 --- /dev/null +++ b/tests/mock_tables/bgpsummary_ipv4.txt @@ -0,0 +1,35 @@ + +Hello, this is Quagga (version 0.99.24.1). +Copyright 1996-2005 Kunihiro Ishiguro, et al. + + User Access Verification + +"Password: +str-msn2700-05> str-msn2700-05> show ip bgp summary +BGP router identifier 10.1.0.32, local AS number 65100 +RIB entries 13025, using 1425 KiB of memory +Peers 16, using 291 KiB of memory + +Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd +10.0.0.57 4 64600 3253 3255 0 0 0 00:48:54 6402 +10.0.0.59 4 64600 3252 3258 0 0 0 00:48:56 6402 +10.0.0.61 4 64600 3253 3256 0 0 0 00:48:55 6402 +10.0.0.63 4 64600 3252 56 0 0 0 00:48:57 6402 +10.0.0.65 4 64016 205 9956 0 0 0 13:21:23 Idle +10.0.0.67 4 65200 3210 3246 0 0 0 00:00:11 Idle (Admin) +fc00::72 4 64600 3253 3255 0 0 0 00:48:54 0 +fc00::76 4 64600 3253 3255 0 0 0 00:48:54 0 +fc00::7a 4 64600 3253 3255 0 0 0 00:48:55 0 +fc00::7e 4 64600 3253 54 0 0 0 00:48:55 0 +fc00::2 4 65200 6608 6790 0 0 0 13:21:22 Active +fc00::4 4 65200 6608 6790 0 0 0 13:21:22 Connect +fc00::6 4 65200 6608 6790 0 0 0 13:21:22 OpenSent +fc00::8 4 65200 6608 6790 0 0 0 13:21:22 OpenConfirm +fc00::10 4 65200 6608 6790 0 0 0 13:21:22 Clearing +fc00::12 4 65200 6608 6790 0 0 0 13:21:22 Deleted + +Total number of neighbors 15 +str-msn2700-05> +str-msn2700-05> exit + + diff --git a/tests/mock_tables/bgpsummary_ipv6.txt b/tests/mock_tables/bgpsummary_ipv6.txt new file mode 100644 index 0000000000..318f3db051 --- /dev/null +++ b/tests/mock_tables/bgpsummary_ipv6.txt @@ -0,0 +1,31 @@ + +Hello, this is Quagga (version 0.99.24.1). +Copyright 1996-2005 Kunihiro Ishiguro, et al. + + +User Access Verification + +"Password: +str-msn2700-05> +BGP router identifier 10.1.0.32, local AS number 65100 +RIB entries 13025, using 1425 KiB of memory +Peers 11, using 291 KiB of memory + +Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd +fc00::72 4 64600 3253 3255 0 0 0 00:48:54 Idle (Admin) +fc00::76 4 64600 3253 3255 0 0 0 00:48:54 0 +fc00::7a 4 64600 3253 3255 0 0 0 00:48:55 0 +fc00::7e 4 64600 3253 54 0 0 0 00:48:55 0 +fc00::2 4 65200 6608 6790 0 0 0 13:21:22 Active +fc00::4 4 65200 6608 6790 0 0 0 13:21:22 Connect +fc00::6 4 65200 6608 6790 0 0 0 13:21:22 OpenSent +fc00::8 4 65200 6608 6790 0 0 0 13:21:22 OpenConfirm +fc00::10 4 65200 6608 6790 0 0 0 13:21:22 Clearing +fc00::12 4 65200 6608 6790 0 0 0 13:21:22 Deleted +2603:10b0:2800:cc1::2e + 4 64611 2919 92 0 0 0 01:18:59 0 + +Total number of neighbors 11 +str-msn2700-05> +str-msn2700-05> exit + diff --git a/tests/mock_tables/bgpsummary_ipv6_nobgp.txt b/tests/mock_tables/bgpsummary_ipv6_nobgp.txt new file mode 100644 index 0000000000..76722aaf3e --- /dev/null +++ b/tests/mock_tables/bgpsummary_ipv6_nobgp.txt @@ -0,0 +1,4 @@ +Hello, this is Quagga (version 0.99.24.1). Copyright 1996-2005 Kunihiro Ishiguro, et al. User Access Verification +"Password: +str-msn2700-03> show ipv6 bgp summary +str-msn2700-03> exit diff --git a/tests/mock_tables/socket.py b/tests/mock_tables/socket.py new file mode 100644 index 0000000000..433ca5586b --- /dev/null +++ b/tests/mock_tables/socket.py @@ -0,0 +1,45 @@ +import os +from collections import namedtuple +import unittest +from unittest import TestCase, mock +from unittest.mock import patch, mock_open, MagicMock + +INPUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +import socket + +# Backup original class +_socket_class = socket.socket + +# Monkey patch +class MockSocket(_socket_class): + + def __init__(self, *args, **kwargs): + super(MockSocket, self).__init__(*args, **kwargs) + self._string_sent = b'' + + def connect(self, *args, **kwargs): + pass + + def send(self, *args, **kwargs): + string = args[0] + self._string_sent = string + pass + + def recv(self, *args, **kwargs): + if b'show ip bgp summary' in self._string_sent: + filename = INPUT_DIR + '/bgpsummary_ipv4.txt' + elif b'show ipv6 bgp summary' in self._string_sent: + filename = INPUT_DIR + '/bgpsummary_ipv6.txt' + else: + return None + + self._string_sent = b'' + ret = namedtuple('ret', ['returncode', 'stdout']) + ret.returncode = 0 + with open(filename, 'rb') as f: + ret = f.read() + return ret + +# Replace the function with mocked one +socket.socket = MockSocket diff --git a/tests/test_vtysh.py b/tests/test_vtysh.py new file mode 100644 index 0000000000..6a82d26c7b --- /dev/null +++ b/tests/test_vtysh.py @@ -0,0 +1,110 @@ +import os +import sys + +modules_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(modules_path, 'src')) + +from unittest import TestCase +from unittest.mock import patch, mock_open + +from ax_interface.mib import MIBTable +from ax_interface.pdu import PDUHeader +from ax_interface.pdu_implementations import GetPDU, GetNextPDU +from ax_interface import ValueType +from ax_interface.encodings import ObjectIdentifier +from ax_interface.constants import PduTypes +from sonic_ax_impl.mibs.ietf import rfc4363 +from sonic_ax_impl.main import SonicMIB +from sonic_ax_impl.lib.vtysh_helper import parse_bgp_summary + +class TestSonicMIB(TestCase): + @classmethod + def setUpClass(cls): + cls.lut = MIBTable(SonicMIB) + for updater in cls.lut.updater_instances: + updater.update_data() + + def test_getpdu_established(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 4, 1, 9, 9, 187, 1, 2, 5, 1, 3, 1, 4, 10, 0, 0, 61)) + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=[oid] + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.INTEGER) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data, 6) + + def test_getpdu_idle(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 4, 1, 9, 9, 187, 1, 2, 5, 1, 3, 1, 4, 10, 0, 0, 65)) + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=[oid] + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.INTEGER) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data, 1) + + def test_getpdu_active(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 4, 1, 9, 9, 187, 1, 2, 5, 1, 3, 2, 16, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2)) + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=[oid] + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.INTEGER) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data, 3) + + def test_getpdu_disappeared(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 4, 1, 9, 9, 187, 1, 2, 5, 1, 3, 2, 16, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10)) + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=[oid] + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.NO_SUCH_INSTANCE) + + def test_getpdu_ipv4_overwite_ipv6(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 4, 1, 9, 9, 187, 1, 2, 5, 1, 3, 2, 16, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x72)) + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=[oid] + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.INTEGER) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data, 6) + + def parse_no_bgp(): + filename = 'bgpsummary_ipv6_nobgp.txt' + with open(filename, 'rb') as f: + bgpsu = f.read() + bgpsumm_ipv6 = parse_bgp_summary(bgpsu) + self.assertEqual(bgpsumm_ipv6, []) \ No newline at end of file