diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index 64f21d7..0dbc129 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -5,6 +5,7 @@ 'pysnmp library is not installed, install it with "pip install pysnmp"' ) +import binascii import logging from netengine.backends import BaseBackend @@ -45,6 +46,17 @@ def _command(self): """ return cmdgen.CommandGenerator() + def _octet_to_mac(self, octet_mac): + """ + returns a valid mac address for a given octetstring + """ + mac_address = binascii.b2a_hex(octet_mac.encode()).decode() + if mac_address is not '': + mac_address = ':'.join( + [mac_address[slice(i, i + 2)] for i in range(0, 12, 2) if i != ''] + ) + return mac_address + def _oid(self, oid): """ returns valid oid value to be passed to getCmd() or nextCmd() diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index ceb57b2..557d5b7 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -5,10 +5,13 @@ __all__ = ['OpenWRT'] -import binascii +import datetime import logging +import struct from datetime import timedelta +import pytz + from netengine.backends.snmp import SNMP logger = logging.getLogger(__name__) @@ -107,16 +110,8 @@ def interfaces_MAC(self): mac1.append(self.get_value('1.3.6.1.2.1.2.2.1.6.' + str(i))) mac_trans = [] for i in mac1: - mac_string = binascii.b2a_hex(i.encode()).decode() - mac_trans.append( - ':'.join( - [ - mac_string[slice(i, i + 2)] - for i in range(0, 12, 2) - if i != '' - ] - ) - ) + mac_string = self._octet_to_mac(i) + mac_trans.append(mac_string) for i in range(0, len(self.get_interfaces())): result = self._dict( {'name': self.get_interfaces()[i], 'mac_address': mac_trans[i]} @@ -205,14 +200,14 @@ def interfaces_speed(self): return self._interfaces_speed - _interfaces_state = None + _interfaces_up = None @property - def interfaces_state(self): + def interfaces_up(self): """ - Returns an ordereed dict with the interfaces and their state (up, down) + Returns an ordereed dict with the interfaces and their state (up: true/false) """ - if self._interfaces_state is None: + if self._interfaces_up is None: results = [] starting = '1.3.6.1.2.1.2.2.1.2.' operative = '1.3.6.1.2.1.2.2.1.8.' @@ -220,26 +215,20 @@ def interfaces_state(self): tmp[18] = str(4) for i in self._value_to_retrieve(): if self.get_value(starting + str(i)) != '': - if int(self.get_value(operative + str(i))) == 1: - result = self._dict( - {'name': self.get_value(starting + str(i)), 'state': 'up'} - ) - results.append(result) - else: - result = self._dict( - { - 'name': self.get_value(starting + str(i)), - 'state': 'down', - } - ) - results.append(result) + result = self._dict( + { + 'name': self.get_value(starting + str(i)), + 'up': int(self.get_value(operative + str(i))) == 1, + } + ) + results.append(result) elif self.get_value(starting + str(i)) == '': result = self._dict({'name': '', 'state': ''}) results.append(result) - self._interfaces_state = results + self._interfaces_up = results - return self._interfaces_state + return self._interfaces_up _interfaces_bytes = None @@ -349,31 +338,81 @@ def interfaces_to_dict(self): rx_bytes = int(self.interfaces_bytes[i]['rx']) logger.info('... tx_bytes ...') tx_bytes = int(self.interfaces_bytes[i]['tx']) - logger.info('... state ...') - state = self.interfaces_state[i]['state'] + logger.info('... up ...') + up = self.interfaces_up[i]['up'] logger.info('... mtu ...') mtu = int(self.interfaces_mtu[i]['mtu']) - logger.info('... speed ...') - speed = int(self.interfaces_speed[i]['speed']) - logger.info('... ip address & subnet ...') - ip_and_netmask = self.interface_addr_and_mask - - if name in list(ip_and_netmask.keys()): - ip_address = ip_and_netmask[name]['address'] - netmask = ip_and_netmask[name]['netmask'] - else: - ip_address = None - netmask = None result = self._dict( { 'name': name, - 'statistics': {'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes,}, + 'statistics': { + "mac": mac_address, + "type": if_type, + "up": up, + 'rx_bytes': rx_bytes, + 'tx_bytes': tx_bytes, + "mtu": mtu, + }, } ) results.append(result) return results + @property + def local_time(self): + """ + returns the local time of the host device as a timestamp + """ + octetstr = bytes(self.get('1.3.6.1.2.1.25.1.2.0')[3][0][1]) + size = len(octetstr) + # string may or may not contain timezone, so size can be 8 or 11 + if size == 8: + (year, month, day, hour, minutes, seconds, deci_seconds,) = struct.unpack( + '>HBBBBBB', octetstr + ) + return int( + datetime.datetime( + year, + month, + day, + hour, + minutes, + seconds, + deci_seconds * 100_000, + tzinfo=pytz.utc, + ).timestamp() + ) + elif size == 11: + ( + year, + month, + day, + hour, + minutes, + seconds, + deci_seconds, + direction, + hours_from_utc, + minutes_from_utc, + ) = struct.unpack('>HBBBBBBcBB', octetstr) + offset = datetime.timedelta(hours=hours_from_utc, minutes=minutes_from_utc) + if direction == b'-': + offset = -offset + return int( + datetime.datetime( + year, + month, + day, + hour, + minutes, + seconds, + deci_seconds * 100_000, + tzinfo=pytz.utc, + ).timestamp() + ) + logger.warning('Invalid timestring was supplied') + @property def RAM_total(self): """ @@ -381,12 +420,110 @@ def RAM_total(self): """ return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.1')) + @property + def RAM_shared(self): + """ + returns the shared RAM of the device + """ + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.8')) + + @property + def RAM_cached(self): + """ + returns the cached RAM of the device + """ + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.7')) + + @property + def RAM_free(self): + """ + returns the free RAM of the device + """ + RAM_used = int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.1')) + RAM_free = self.RAM_total - (self.RAM_cached + RAM_used) + return RAM_free + + @property + def SWAP_total(self): + """ + returns the total SWAP of the device + """ + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.10')) + + @property + def SWAP_free(self): + """ + returns the free SWAP of the device + """ + SWAP_used = int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.10')) + SWAP_free = self.SWAP_total - SWAP_used + return SWAP_free + + @property + def CPU_count(self): + """ + returns the count of CPUs of the device + """ + return len(self.next('1.3.6.1.2.1.25.3.3.1.2')[3]) + + @property + def resources_to_dict(self): + """ + returns an ordered dict with hardware resources information + """ + result = self._dict( + { + 'cpus': self.CPU_count, + 'memory': { + 'total': self.RAM_total, + 'shared': self.RAM_shared, + 'free': self.RAM_free, + 'cached': self.RAM_cached, + }, + 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free,}, + } + ) + return result + + @property + def neighbors(self): + """ + returns a dict with neighbors information + """ + states_map = { + '1': 'REACHABLE', + '2': 'STALE', + '3': 'DELAY', + '4': 'PROBE', + '5': 'INVALID', + '6': 'UNKNOWN', + '7': 'INCOMPLETE', + } + + # TODO: find a way to extract IP address from the OID + neighbors = self.next('1.3.6.1.2.1.4.35.1.4')[3] + neighbor_states = self.next('1.3.6.1.2.1.4.35.1.7')[3] + result = [] + + for i in range(len(neighbors)): + interface_num = neighbors[i][0][0].getOid()[10] + interface = self.get(f'1.3.6.1.2.1.31.1.1.1.1.{interface_num}')[3][0][1] + state = states_map[str(neighbor_states[i][0][1])] + mac = self._octet_to_mac(str(neighbors[i][0][1])) + result.append( + self._dict( + {'mac': str(mac), 'state': str(state), 'interface': str(interface),} + ) + ) + return result + def to_dict(self): return self._dict( { 'type': 'DeviceMonitoring', - 'general': {'uptime': self.uptime,}, - 'resources': {'memory': {'total': self.RAM_total,}}, + 'general': {'uptime': self.uptime, "local_time": self.local_time}, + 'resources': self.resources_to_dict, 'interfaces': self.interfaces_to_dict, + 'neighbors': self.neighbors, } ) diff --git a/tests/static/test-openwrt-snmp-oid.json b/tests/static/test-openwrt-snmp-oid.json index 2db1140..94da882 100644 --- a/tests/static/test-openwrt-snmp-oid.json +++ b/tests/static/test-openwrt-snmp-oid.json @@ -45,5 +45,11 @@ "1.3.6.1.2.1.2.2.1.6.2": "\b\u0000''�\u0010\u0000", "1.3.6.1.2.1.2.2.1.6.3": "\b\u0000''�\u0010\u0014", "1.3.6.1.2.1.2.2.1.6.4": "\b\u0000''�\u0010\u0000", - "1.3.6.1.2.1.2.2.1.6.5": "\b\u0000''�\u0010\u0015" + "1.3.6.1.2.1.2.2.1.6.5": "\b\u0000''�\u0010\u0015", + "1.3.6.1.2.1.25.2.3.1.5.7": "6956", + "1.3.6.1.2.1.25.2.3.1.6.1": "25164", + "1.3.6.1.2.1.25.2.3.1.6.8": "88", + "1.3.6.1.2.1.25.2.3.1.6.10": "0", + "1.3.6.1.2.1.25.2.3.1.5.10": "0", + "1.3.6.1.2.1.25.1.2.0": "\u0007\u00e5\u0006\t14\ufffd+\ufffd\ufffd" } diff --git a/tests/test_snmp/test_base.py b/tests/test_snmp/test_base.py index 7c7a82c..a001982 100644 --- a/tests/test_snmp/test_base.py +++ b/tests/test_snmp/test_base.py @@ -13,6 +13,7 @@ def setUp(self): self.host = settings['base-snmp']['host'] self.community = settings['base-snmp']['community'] self.port = settings['base-snmp'].get('port', 161) + self.device = SNMP(self.host, self.community, self.port) def test_instantiation(self): device = SNMP(self.host, self.community, self.port) @@ -65,3 +66,17 @@ class RightSNMPBackend(SNMP): # now we expect a different kind of error with self.assertRaises(IndexError): device._value_to_retrieve() + + def test_octet_to_mac(self): + self.assertEqual( + self.device._octet_to_mac('\x04\x0e<\xcaU_'), '04:0e:3c:c3:8a:55' + ) + + def test_oid(self): + self.assertEqual(self.device._oid('1,3,6,1,2,1,1,5,0'), '1.3.6.1.2.1.1.5.0') + self.assertEqual( + self.device._oid('1, 3, 6, 1, 2, 1, 1, 5, 0'), '1.3.6.1.2.1.1.5.0', + ) + self.assertEqual( + self.device._oid([1, 3, 6, 1, 2, 1, 1, 5, 0]), '1.3.6.1.2.1.1.5.0', + ) diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index e73f108..10cfe1e 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -80,7 +80,7 @@ def test_interfaces_mtu(self): def test_interfaces_state(self): with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_state, list) + self.assertIsInstance(self.device.interfaces_up, list) def test_interfaces_to_dict(self): with self.nextcmd_patcher as p: @@ -95,11 +95,32 @@ def test_interface_addr_and_mask(self): def test_RAM_total(self): self.assertIsInstance(self.device.RAM_total, int) + def test_RAM_shared(self): + self.assertIsInstance(self.device.RAM_shared, int) + + def test_RAM_cached(self): + self.assertIsInstance(self.device.RAM_cached, int) + + def test_RAM_free(self): + self.assertIsInstance(self.device.RAM_free, int) + + def test_SWAP_total(self): + self.assertIsInstance(self.device.SWAP_total, int) + + def test_SWAP_free(self): + self.assertIsInstance(self.device.SWAP_free, int) + + def test_CPU_count(self): + self.assertIsInstance(self.device.CPU_count, int) + + def test_neighbors(self): + self.assertIsInstance(self.device.neighbors, list) + def test_to_dict(self): with self.nextcmd_patcher as p: SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, []]) device_dict = self.device.to_dict() - self.assertTrue(isinstance(device_dict, dict)) + self.assertIsInstance(device_dict, dict) self.assertEqual( len(device_dict['interfaces']), len(self.device.get_interfaces()), )