diff --git a/setup.py b/setup.py index 233356e25..eeabc484b 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ dependencies = [ 'sswsdk>=2.0.1', 'psutil>=4.0', + 'python_arptable>=0.0.1', ] test_deps = [ diff --git a/src/ax_interface/encodings.py b/src/ax_interface/encodings.py index 0d6b616d2..c074be4a6 100644 --- a/src/ax_interface/encodings.py +++ b/src/ax_interface/encodings.py @@ -143,12 +143,14 @@ def size(self): return 4 + self.length + util.pad4(self.length) def __str__(self): - return self.string.decode('ascii') + # Note: ascii encoding (0-0x7F) is not enough to decode the internal bytes (self.string) + # “latin-1” encoding maps byte values directly to the first 256 Unicode code points + return self.string.decode('latin-1') @classmethod def from_string(cls, string): length = len(string) - _string = bytes(string, 'ascii') if type(string) is str else string + _string = bytes(string, 'latin-1') if type(string) is str else string return cls(length, _string, util.pad4bytes(len(_string))) def to_bytes(self, endianness): diff --git a/src/ax_interface/util.py b/src/ax_interface/util.py index 10e4823f9..e5122328b 100644 --- a/src/ax_interface/util.py +++ b/src/ax_interface/util.py @@ -98,3 +98,10 @@ def mac_decimals(mac): """ return tuple(int(h, 16) for h in mac.split(":")) +def ip2tuple_v4(ip): + """ + >>> ip2tuple_v4("192.168.1.253") + (192, 168, 1, 253) + """ + return tuple(int(bs) for bs in str(ip).split('.')) + diff --git a/src/sonic_ax_impl/main.py b/src/sonic_ax_impl/main.py index 230986060..d2144fc27 100644 --- a/src/sonic_ax_impl/main.py +++ b/src/sonic_ax_impl/main.py @@ -23,6 +23,7 @@ class SonicMIB( rfc1213.InterfacesMIB, + rfc1213.IpMib, rfc2863.InterfaceMIBObjects, rfc4363.QBridgeMIBObjects, rfc4292.IpCidrRouteTable, diff --git a/src/sonic_ax_impl/mibs/__init__.py b/src/sonic_ax_impl/mibs/__init__.py index 20984ff8a..a4c64beed 100644 --- a/src/sonic_ax_impl/mibs/__init__.py +++ b/src/sonic_ax_impl/mibs/__init__.py @@ -34,7 +34,14 @@ def get_index(if_name): OIDs are 1-based, interfaces are 0-based, return the 1-based index Ethernet N = N + 1 """ - match = re.match(SONIC_ETHERNET_RE_PATTERN, if_name.decode()) + return get_index_from_str(if_name.decode()) + +def get_index_from_str(if_name): + """ + OIDs are 1-based, interfaces are 0-based, return the 1-based index + Ethernet N = N + 1 + """ + match = re.match(SONIC_ETHERNET_RE_PATTERN, if_name) if match: n = match.group(1) return int(n) + 1 diff --git a/src/sonic_ax_impl/mibs/ietf/rfc1213.py b/src/sonic_ax_impl/mibs/ietf/rfc1213.py index 3c92b3733..19cc826dc 100644 --- a/src/sonic_ax_impl/mibs/ietf/rfc1213.py +++ b/src/sonic_ax_impl/mibs/ietf/rfc1213.py @@ -1,8 +1,11 @@ +import python_arptable from enum import unique, Enum +from bisect import bisect_right from sonic_ax_impl import mibs -from ax_interface import MIBMeta, ValueType, MIBUpdater, MIBEntry, ContextualMIBEntry +from ax_interface import MIBMeta, ValueType, MIBUpdater, MIBEntry, ContextualMIBEntry, SubtreeMIBEntry from ax_interface.encodings import ObjectIdentifier +from ax_interface.util import mac_decimals, ip2tuple_v4 @unique @@ -41,6 +44,51 @@ class DbTables(int, Enum): # ifOutQLen ::= { ifEntry 21 } SAI_PORT_STAT_IF_OUT_QLEN = 21 +class ArpUpdater(MIBUpdater): + def __init__(self): + super().__init__() + self.arp_dest_map = {} + self.arp_dest_list = [] + # call our update method once to "seed" data before the "Agent" starts accepting requests. + self.update_data() + + def update_data(self): + self.arp_dest_map = {} + self.arp_dest_list = [] + for entry in python_arptable.get_arp_table(): + dev = entry['Device'] + mac = entry['HW address'] + ip = entry['IP address'] + + if_index = mibs.get_index_from_str(dev) + if if_index is None: continue + + mactuple = mac_decimals(mac) + machex = ''.join(chr(b) for b in mactuple) + # if MAC is all zero + #if not any(mac): continue + + iptuple = ip2tuple_v4(ip) + + subid = (if_index,) + iptuple + self.arp_dest_map[subid] = machex + self.arp_dest_list.append(subid) + self.arp_dest_list.sort() + + def arp_dest(self, sub_id): + return self.arp_dest_map.get(sub_id, None) + + def get_next(self, sub_id): + right = bisect_right(self.arp_dest_list, sub_id) + if right >= len(self.arp_dest_list): + return None + return self.arp_dest_list[right] + +class IpMib(metaclass=MIBMeta, prefix='.1.3.6.1.2.1.4'): + arp_updater = ArpUpdater() + + ipNetToMediaPhysAddress = \ + SubtreeMIBEntry('22.1.2', arp_updater, ValueType.OCTET_STRING, arp_updater.arp_dest) class InterfacesUpdater(MIBUpdater): def __init__(self): diff --git a/src/sonic_ax_impl/mibs/ietf/rfc4292.py b/src/sonic_ax_impl/mibs/ietf/rfc4292.py index 2bcd31ebf..04392b65f 100644 --- a/src/sonic_ax_impl/mibs/ietf/rfc4292.py +++ b/src/sonic_ax_impl/mibs/ietf/rfc4292.py @@ -5,12 +5,9 @@ from sonic_ax_impl import mibs from ax_interface import MIBMeta, ValueType, MIBUpdater, ContextualMIBEntry, SubtreeMIBEntry from ax_interface.encodings import OctetString -from ax_interface.util import mac_decimals +from ax_interface.util import mac_decimals, ip2tuple_v4 from bisect import bisect_right -def ip2tuple(ip): - return tuple(int(bs) for bs in str(ip).split('.')) - class RouteUpdater(MIBUpdater): def __init__(self): super().__init__() @@ -37,7 +34,7 @@ def update_data(self): ent = self.db_conn.get_all(mibs.APPL_DB, routestr, blocking=True) nexthops = ent[b"nexthop"].decode() for nh in nexthops.split(','): - sub_id = ip2tuple(ipn.network_address) + ip2tuple(ipn.netmask) + (self.tos,) + ip2tuple(nh) + sub_id = ip2tuple_v4(ipn.network_address) + ip2tuple_v4(ipn.netmask) + (self.tos,) + ip2tuple_v4(nh) self.route_dest_list.append(sub_id) self.route_dest_map[sub_id] = ipn.network_address.packed diff --git a/src/sonic_ax_impl/mibs/ietf/rfc4363.py b/src/sonic_ax_impl/mibs/ietf/rfc4363.py index d7cf78153..9a21bae21 100644 --- a/src/sonic_ax_impl/mibs/ietf/rfc4363.py +++ b/src/sonic_ax_impl/mibs/ietf/rfc4363.py @@ -48,7 +48,7 @@ def update_data(self): vlanmac = fdb_vlanmac(fdb) self.vlanmac_ifindex_map[vlanmac] = mibs.get_index(self.if_id_map[port_oid]) self.vlanmac_ifindex_list.append(vlanmac) - self.vlanmac_ifindex_list.sort() + self.vlanmac_ifindex_list.sort() def fdb_ifindex(self, sub_id): diff --git a/tests/mock_tables/arp.txt b/tests/mock_tables/arp.txt new file mode 100644 index 000000000..04ae9939b --- /dev/null +++ b/tests/mock_tables/arp.txt @@ -0,0 +1,78 @@ +IP address HW type Flags HW address Mask Device +10.3.146.86 0x1 0x2 00:a0:a5:75:35:8d * eth0 +10.0.0.35 0x1 0x2 52:54:00:a5:70:47 * Ethernet68 +10.0.0.19 0x1 0x2 52:54:00:04:52:5d * Ethernet36 +10.0.0.57 0x1 0x2 52:54:00:b4:59:59 * Ethernet112 +10.3.146.1 0x1 0x2 00:00:5e:00:01:64 * eth0 +10.3.146.244 0x1 0x2 e4:d3:f1:51:3c:80 * eth0 +10.3.147.97 0x1 0x2 3c:94:d5:69:b5:02 * eth0 +10.0.0.25 0x1 0x2 52:54:00:55:2d:fe * Ethernet48 +10.3.146.75 0x1 0x2 00:a0:a5:76:17:70 * eth0 +10.3.146.131 0x1 0x2 00:15:c7:21:f7:40 * eth0 +10.0.0.21 0x1 0x2 52:54:00:d0:a0:8c * Ethernet40 +10.3.147.223 0x1 0x2 00:17:0f:ac:c4:40 * eth0 +10.3.146.78 0x1 0x2 3c:94:d5:68:9d:82 * eth0 +10.3.146.184 0x1 0x2 e4:d3:f1:51:38:60 * eth0 +10.3.146.170 0x1 0x2 30:e4:db:a4:c9:3f * eth0 +10.0.0.33 0x1 0x2 7c:fe:90:5e:6b:a6 * Ethernet64 +10.3.146.134 0x1 0x2 00:23:04:18:8e:c0 * eth0 +10.3.146.15 0x1 0x2 00:1e:f7:f7:0a:80 * eth0 +10.3.146.85 0x1 0x2 00:a0:a5:80:38:22 * eth0 +10.0.0.45 0x1 0x2 52:54:00:d0:23:2b * Ethernet88 +10.3.147.40 0x1 0x2 3c:94:d5:68:4a:82 * eth0 +10.0.0.59 0x1 0x2 52:54:00:ae:c8:01 * Ethernet116 +10.3.146.74 0x1 0x2 00:a0:a5:7a:39:ea * eth0 +10.0.0.5 0x1 0x2 52:54:00:fc:50:3c * Ethernet8 +10.3.146.130 0x1 0x2 00:15:c7:21:dd:00 * eth0 +10.3.146.81 0x1 0x2 5c:5e:ab:de:70:ff * eth0 +10.0.0.55 0x1 0x2 52:54:00:1b:d6:95 * Ethernet108 +10.0.0.11 0x1 0x0 00:00:00:00:00:00 * Ethernet20 +10.3.146.14 0x1 0x2 00:1e:f7:f7:14:40 * eth0 +10.0.0.61 0x1 0x0 00:00:00:00:00:00 * Ethernet120 +10.0.0.3 0x1 0x0 00:00:00:00:00:00 * Ethernet4 +10.3.146.70 0x1 0x2 00:a0:a5:77:ef:f1 * eth0 +10.3.146.162 0x1 0x2 58:8d:09:8c:3c:bf * eth0 +10.3.146.172 0x1 0x2 a4:93:4c:da:f7:bf * eth0 +10.0.0.29 0x1 0x0 00:00:00:00:00:00 * Ethernet56 +10.3.146.95 0x1 0x2 00:a0:a5:85:f8:98 * eth0 +10.3.146.187 0x1 0x2 6c:20:56:cb:20:40 * eth0 +10.0.0.31 0x1 0x0 00:00:00:00:00:00 * Ethernet60 +10.3.146.10 0x1 0x2 4c:76:25:eb:52:42 * eth0 +10.3.147.225 0x1 0x2 00:22:91:86:10:00 * eth0 +10.0.0.53 0x1 0x2 52:54:00:c3:a1:2d * Ethernet104 +10.0.0.41 0x1 0x2 52:54:00:08:de:c3 * Ethernet80 +10.3.146.190 0x1 0x2 e4:d3:f1:51:33:20 * eth0 +10.0.0.15 0x1 0x2 52:54:00:c6:31:42 * Ethernet28 +10.0.0.43 0x1 0x2 52:54:00:44:73:a4 * Ethernet84 +10.3.146.3 0x1 0x2 f4:b5:2f:72:bf:f0 * eth0 +10.3.147.250 0x1 0x2 4c:76:25:f4:c6:02 * eth0 +10.0.0.49 0x1 0x2 52:54:00:74:c5:38 * Ethernet96 +10.3.146.91 0x1 0x2 54:e0:32:cf:6f:ff * eth0 +10.0.0.63 0x1 0x0 00:00:00:00:00:00 * Ethernet124 +10.0.0.27 0x1 0x2 52:54:00:1c:90:c4 * Ethernet52 +10.0.0.9 0x1 0x2 52:54:00:71:ae:0e * Ethernet16 +10.3.146.157 0x1 0x2 00:1e:be:38:44:ff * eth0 +10.3.147.239 0x1 0x2 ec:f4:bb:fe:80:a1 * eth0 +10.0.0.7 0x1 0x2 52:54:00:d1:75:b4 * Ethernet12 +10.3.146.72 0x1 0x2 00:a0:a5:80:26:07 * eth0 +10.3.146.164 0x1 0x2 00:15:c6:df:03:7f * eth0 +10.3.146.150 0x1 0x2 00:05:9b:7e:61:00 * eth0 +10.3.147.224 0x1 0x2 00:22:91:85:88:00 * eth0 +10.3.146.87 0x1 0x2 00:a0:a5:80:2c:7a * eth0 +10.0.0.37 0x1 0x2 52:54:00:30:a7:95 * Ethernet72 +10.3.146.16 0x1 0x2 ec:bd:1d:f2:a6:00 * eth0 +10.0.0.51 0x1 0x2 52:54:00:36:5b:05 * Ethernet100 +10.3.146.2 0x1 0x2 f4:b5:2f:79:b3:f0 * eth0 +10.0.0.1 0x1 0x2 7c:fe:90:5e:6b:a6 * Ethernet0 +10.0.0.13 0x1 0x2 52:54:00:88:3c:5e * Ethernet24 +10.3.146.90 0x1 0x2 54:e0:32:cf:76:ff * eth0 +10.3.146.182 0x1 0x2 6c:20:56:ee:c0:80 * eth0 +10.0.0.47 0x1 0x2 52:54:00:77:e3:91 * Ethernet92 +10.3.146.156 0x1 0x2 00:18:73:b1:7d:bf * eth0 +10.0.0.23 0x1 0x2 52:54:00:df:ec:bf * Ethernet44 +10.3.146.83 0x1 0x2 00:a0:a5:87:1d:28 * eth0 +10.3.146.93 0x1 0x2 54:e0:32:cf:77:ff * eth0 +10.3.146.79 0x1 0x2 3c:94:d5:60:40:c2 * eth0 +10.3.146.171 0x1 0x2 c8:9c:1d:ee:7f:7f * eth0 +10.0.0.39 0x1 0x2 52:54:00:66:a7:29 * Ethernet76 +10.0.0.17 0x1 0x2 52:54:00:e7:53:c2 * Ethernet32 diff --git a/tests/mock_tables/python_arptable.py b/tests/mock_tables/python_arptable.py new file mode 100644 index 000000000..568650be7 --- /dev/null +++ b/tests/mock_tables/python_arptable.py @@ -0,0 +1,25 @@ +import os +import csv +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 python_arptable + +# Backup original function +_get_arp_table = getattr(python_arptable, 'get_arp_table') + +# Monkey patch +def get_arp_table(): + with open(INPUT_DIR + '/arp.txt') as farp: + file_content = mock_open(read_data = farp.read()) + file_content.return_value.__iter__ = lambda self : iter(self.readline, '') + # file_content = MagicMock(name = 'open', spec = open) + # file_content.return_value = iter(farp.readlines()) + with patch('builtins.open', file_content): + return _get_arp_table() + +# Replace the function with mocked one +python_arptable.get_arp_table = get_arp_table diff --git a/tests/test_arp.py b/tests/test_arp.py new file mode 100644 index 000000000..34c29d2af --- /dev/null +++ b/tests/test_arp.py @@ -0,0 +1,111 @@ +import os +import sys +import mock + +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 + +# noinspection PyUnresolvedReferences +import tests.mock_tables.dbconnector +import tests.mock_tables.python_arptable +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 + +class TestSonicMIB(TestCase): + @classmethod + def setUpClass(cls): + cls.lut = MIBTable(SonicMIB) + + def test_getpdu(self): + oid = ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 37, 10, 0, 0, 19)) + 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.OCTET_STRING) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data.string, b'\x52\x54\x00\x04\x52\x5d') + + def test_getnextpdu(self): + get_pdu = GetNextPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=( + ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 37, 10, 0, 0, 19)), + ) + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + n = len(response.values) + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.OCTET_STRING) + self.assertEqual(value0.data.string, b'\x52\x54\x00\xd0\xa0\x8c') + + def test_getnextpdu_exactmatch(self): + # oid.include = 1 + oid = ObjectIdentifier(20, 0, 1, 0, (1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 37, 10, 0, 0, 19)) + get_pdu = GetNextPDU( + 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) + + n = len(response.values) + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.OCTET_STRING) + print("test_getnextpdu_exactmatch: ", str(oid)) + self.assertEqual(str(value0.name), str(oid)) + self.assertEqual(value0.data.string, b'\x52\x54\x00\x04\x52\x5d') + + def test_getpdu_noinstance(self): + get_pdu = GetPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=( + ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 2, 1, 4, 22, 1, 2, 39)), + ) + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + n = len(response.values) + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.NO_SUCH_INSTANCE) + + def test_getnextpdu_empty(self): + get_pdu = GetNextPDU( + header=PDUHeader(1, PduTypes.GET, 16, 0, 42, 0, 0, 0), + oids=( + ObjectIdentifier(20, 0, 0, 0, (1, 3, 6, 1, 2, 1, 4, 22, 1, 3)), + ) + ) + + encoded = get_pdu.encode() + response = get_pdu.make_response(self.lut) + print(response) + + n = len(response.values) + value0 = response.values[0] + self.assertEqual(value0.type_, ValueType.END_OF_MIB_VIEW) +