From d46b5685c4bf4f63a678687fb4cde8e499f6c9cd Mon Sep 17 00:00:00 2001 From: purhan Date: Sat, 5 Jun 2021 19:29:44 +0530 Subject: [PATCH 01/20] [change] NetJSON DeviceMonitoring compliance #2 Closes #2 --- netengine/backends/schema.py | 152 +++++++++++++++++++++++++++++ netengine/backends/snmp/airos.py | 30 ++---- netengine/backends/snmp/openwrt.py | 26 +---- requirements-test.txt | 1 + tests/static/test-airos-snmp.json | 8 +- tests/test_snmp/test_airos.py | 10 +- tests/test_snmp/test_openwrt.py | 14 ++- tests/utils.py | 7 +- 8 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 netengine/backends/schema.py diff --git a/netengine/backends/schema.py b/netengine/backends/schema.py new file mode 100644 index 0000000..af707f0 --- /dev/null +++ b/netengine/backends/schema.py @@ -0,0 +1,152 @@ +# NetJSON DeviceMonitoring schema, +# https://github.com/netjson/netjson/blob/master/schema/device-monitoring.json +schema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$id': 'https://raw.githubusercontent.com/netjson/netjson/master/schema/device-monitoring.json', + 'title': 'NetJSON Device Monitoring', + 'description': 'Monitoring information sent by a device.', + 'type': 'object', + 'additionalProperties': True, + 'required': ['type'], + 'properties': { + 'type': {'type': 'string', 'enum': ['DeviceMonitoring']}, + 'general': { + 'type': 'object', + 'title': 'General', + 'additionalProperties': True, + 'properties': { + 'local_time': {'type': 'integer'}, + 'uptime': {'type': 'integer'}, + }, + }, + 'resources': { + 'type': 'object', + 'title': 'Resources', + 'additionalProperties': True, + 'properties': { + 'load': { + 'type': 'array', + 'items': {'type': 'number', 'minItems': 3, 'maxItems': 3}, + }, + 'memory': { + 'id': 'memory', + 'type': 'object', + 'properties': { + 'total': {'type': 'integer'}, + 'free': {'type': 'integer'}, + 'buffered': {'type': 'integer'}, + 'cache': {'type': 'integer'}, + }, + }, + 'swap': { + 'type': 'object', + 'properties': { + 'total': {'type': 'integer'}, + 'free': {'type': 'integer'}, + }, + }, + 'connections': { + 'type': 'object', + 'properties': { + 'ipv4': { + 'type': 'object', + 'properties': { + 'tcp': {'type': 'integer'}, + 'udp': {'type': 'integer'}, + }, + }, + 'ipv6': { + 'type': 'object', + 'properties': { + 'tcp': {'type': 'integer'}, + 'udp': {'type': 'integer'}, + }, + }, + }, + }, + 'processes': { + 'type': 'object', + 'properties': { + 'running': {'type': 'integer'}, + 'sleeping': {'type': 'integer'}, + 'blocked': {'type': 'integer'}, + 'zombie': {'type': 'integer'}, + 'stopped': {'type': 'integer'}, + 'paging': {'type': 'integer'}, + }, + }, + 'cpu': { + 'type': 'object', + 'properties': { + 'frequency': {'type': 'integer'}, + 'user': {'type': 'integer'}, + 'system': {'type': 'integer'}, + 'nice': {'type': 'integer'}, + 'idle': {'type': 'integer'}, + 'iowait': {'type': 'integer'}, + 'irq': {'type': 'integer'}, + 'softirq': {'type': 'integer'}, + }, + }, + 'flash': { + 'type': 'object', + 'properties': { + 'total': {'type': 'integer'}, + 'free': {'type': 'integer'}, + }, + }, + 'storage': { + 'type': 'object', + 'properties': { + 'total': {'type': 'integer'}, + 'free': {'type': 'integer'}, + }, + }, + }, + }, + 'interfaces': { + 'type': 'array', + 'title': 'Interfaces', + 'uniqueItems': True, + 'additionalItems': True, + 'items': { + 'type': 'object', + 'title': 'Interface', + 'additionalProperties': True, + 'required': ['name'], + 'properties': { + 'name': {'type': 'string'}, + 'uptime': {'type': 'integer'}, + 'statistics': { + 'type': 'object', + 'properties': { + 'collisions': {'type': 'integer'}, + 'rx_frame_errors': {'type': 'integer'}, + 'tx_compressed': {'type': 'integer'}, + 'multicast': {'type': 'integer'}, + 'rx_length_errors': {'type': 'integer'}, + 'tx_dropped': {'type': 'integer'}, + 'rx_bytes': {'type': 'integer'}, + 'rx_missed_errors': {'type': 'integer'}, + 'tx_errors': {'type': 'integer'}, + 'rx_compressed': {'type': 'integer'}, + 'rx_over_errors': {'type': 'integer'}, + 'tx_fifo_errors': {'type': 'integer'}, + 'rx_crc_errors': {'type': 'integer'}, + 'rx_packets': {'type': 'integer'}, + 'tx_heartbeat_errors': {'type': 'integer'}, + 'rx_dropped': {'type': 'integer'}, + 'tx_aborted_errors': {'type': 'integer'}, + 'tx_packets': {'type': 'integer'}, + 'rx_errors': {'type': 'integer'}, + 'tx_bytes': {'type': 'integer'}, + 'tx_window_errors': {'type': 'integer'}, + 'rx_fifo_errors': {'type': 'integer'}, + 'tx_carrier_errors': {'type': 'integer'}, + }, + }, + }, + }, + }, + }, +} diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 24c7fd1..fce0844 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -317,13 +317,10 @@ def interfaces_to_dict(self): result = self._dict( { 'name': self.interfaces_MAC[i]['name'], - 'type': self.interfaces_type[i]['type'], - 'mac_address': self.interfaces_MAC[i]['mac_address'], - 'rx_bytes': int(self.interfaces_bytes[i]['rx']), - 'tx_bytes': int(self.interfaces_bytes[i]['tx']), - 'state': self.interfaces_state[i]['state'], - 'mtu': int(self.interfaces_mtu[i]['mtu']), - 'speed': int(self.interfaces_speed[i]['speed']), + 'statistics': { + 'rx_bytes': int(self.interfaces_bytes[i]['rx']), + 'tx_bytes': int(self.interfaces_bytes[i]['tx']), + }, } ) results.append(result) @@ -403,20 +400,11 @@ def RAM_free(self): def to_dict(self): return self._dict( { - 'name': self.name, - 'type': 'radio', - 'os': self.os[0], - 'os_version': self.os[1], - 'manufacturer': self.manufacturer, - 'model': self.model, - 'RAM_total': self.RAM_total, - 'RAM_free': self.RAM_free, - 'uptime': self.uptime, - 'uptime_tuple': self.uptime_tuple, + 'type': 'DeviceMonitoring', + 'general': {'uptime': self.uptime,}, + 'resources': { + 'memory': {'total': self.RAM_total, 'free': self.RAM_free,}, + }, 'interfaces': self.interfaces_to_dict, - 'antennas': [], - 'wireless_dbm': self.wireless_dbm, - 'wireless_links': self.wireless_links, - 'routing_protocols': None, } ) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 2e21adb..ceb57b2 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -368,15 +368,7 @@ def interfaces_to_dict(self): result = self._dict( { 'name': name, - 'type': if_type, - 'mac_address': mac_address, - 'ip_address': ip_address, - 'netmask': netmask, - 'rx_bytes': rx_bytes, - 'tx_bytes': tx_bytes, - 'state': state, - 'mtu': mtu, - 'speed': speed, + 'statistics': {'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes,}, } ) results.append(result) @@ -392,17 +384,9 @@ def RAM_total(self): def to_dict(self): return self._dict( { - 'name': self.name, - 'type': 'radio', - 'os': self.os[0], - 'os_version': self.os[1], - 'manufacturer': self.manufacturer, - 'model': None, - 'RAM_total': self.RAM_total, - 'uptime': self.uptime, - 'uptime_tuple': self.uptime_tuple, - 'interfaces': self.get_interfaces(), - 'antennas': [], - 'routing_protocols': None, + 'type': 'DeviceMonitoring', + 'general': {'uptime': self.uptime,}, + 'resources': {'memory': {'total': self.RAM_total,}}, + 'interfaces': self.interfaces_to_dict, } ) diff --git a/requirements-test.txt b/requirements-test.txt index fed2167..136ff05 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,3 +3,4 @@ coverage~=5.5 sphinx~=4.0.2 openwisp-utils[qa]~=0.7.4 pylinkvalidator~=0.3.0 +jsonschema~=3.2.0 diff --git a/tests/static/test-airos-snmp.json b/tests/static/test-airos-snmp.json index e470631..6890337 100644 --- a/tests/static/test-airos-snmp.json +++ b/tests/static/test-airos-snmp.json @@ -8,10 +8,10 @@ "1.2.840.10036.3.1.2.1.4.5": "XM.ar7240.v5.5.12536.120406.1455", "1.2.840.10036.3.1.2.1.3.5": "NanoStation Loco M2", "1.3.6.1.2.1.2.2.1.2.1": "\\xd4\\xa0*", - "1.3.6.1.2.1.2.2.1.2.2": "\\xd4\\xa0*", - "1.3.6.1.2.1.2.2.1.2.3": "\\xd4\\xa0*", - "1.3.6.1.2.1.2.2.1.2.4": "\\xd4\\xa0*", - "1.3.6.1.2.1.2.2.1.2.5": "\\xd4\\xa0*", + "1.3.6.1.2.1.2.2.1.2.2": "\\xd4\\xb0*", + "1.3.6.1.2.1.2.2.1.2.3": "\\xd4\\xc0*", + "1.3.6.1.2.1.2.2.1.2.4": "\\xd4\\xd0*", + "1.3.6.1.2.1.2.2.1.2.5": "\\xd4\\xe0*", "1.3.6.1.2.1.2.2.1.6.1": "\b\u0000''�\u0010\u0014", "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\u0015", diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index 4ea3227..ab19a75 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -1,9 +1,12 @@ +import json import unittest from unittest.mock import patch +from jsonschema import validate from pysnmp.entity.rfc3413.oneliner import cmdgen from pysnmp.smi.error import NoSuchObjectError +from netengine.backends.schema import schema from netengine.backends.snmp import AirOS from netengine.exceptions import NetEngineError @@ -142,7 +145,7 @@ def test_to_dict(self): ) self.assertTrue(isinstance(self.device.to_dict(), dict)) - def test_manufacturer_to_dict(self): + def test_netjson_compliance(self): with self.nextcmd_patcher as np: SpyMock._update_patch( np, @@ -150,7 +153,10 @@ def test_manufacturer_to_dict(self): data=args ), ) - self.assertIsNotNone(self.device.to_dict()['manufacturer']) + device_dict = self.device.to_dict() + device_json = self.device.to_json() + validate(instance=device_dict, schema=schema) + validate(instance=json.loads(device_json), schema=schema) def test_manufacturer(self): with self.nextcmd_patcher: diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index f12588e..e73f108 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -1,8 +1,11 @@ +import json import unittest from unittest.mock import patch +from jsonschema import validate from pysnmp.entity.rfc3413.oneliner import cmdgen +from netengine.backends.schema import schema from netengine.backends.snmp import OpenWRT from ..settings import settings @@ -94,17 +97,20 @@ def test_RAM_total(self): def test_to_dict(self): with self.nextcmd_patcher as p: - SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, [[[0, 1]]] * 5]) + SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, []]) device_dict = self.device.to_dict() self.assertTrue(isinstance(device_dict, dict)) self.assertEqual( len(device_dict['interfaces']), len(self.device.get_interfaces()), ) - def test_manufacturer_to_dict(self): + def test_netjson_compliance(self): with self.nextcmd_patcher as p: - SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, [[[0, 1]]] * 5]) - self.assertIsNotNone(self.device.to_dict()['manufacturer']) + SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, []]) + device_dict = self.device.to_dict() + device_json = self.device.to_json() + validate(instance=device_dict, schema=schema) + validate(instance=json.loads(device_json), schema=schema) def tearDown(self): patch.stopall() diff --git a/tests/utils.py b/tests/utils.py index 6c3b548..aca6018 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -43,6 +43,11 @@ def _get_mocked_wireless_links(data): '1.3.6.1.4.1.14988.1.1.1.2.1': [0, 0, 0, [[[0, 0], 0]] * 28], '1.3.6.1.4.1.14988.1.1.1.2.1.3': [0, 0, 0, [0, 0]], '1.3.6.1.4.1.14988.1.1.1.2.1.3.0': [None, 0, 0, []], - '1.3.6.1.2.1.1.9.1.1': [0, 0, 0, [[[0, 1]]] * 5], + '1.3.6.1.2.1.1.9.1.1': [ + 0, + 0, + 0, + [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + ], } return return_data[oid] From 9168a274c92a134dbde76920fe9777bf827e731f Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 10 Jun 2021 15:34:16 +0530 Subject: [PATCH 02/20] [change] Add monitoring info to OpenWRT backend --- netengine/backends/snmp/base.py | 12 ++ netengine/backends/snmp/openwrt.py | 229 +++++++++++++++++++----- tests/static/test-openwrt-snmp-oid.json | 8 +- tests/test_snmp/test_base.py | 15 ++ tests/test_snmp/test_openwrt.py | 25 ++- 5 files changed, 240 insertions(+), 49 deletions(-) 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()), ) From cfa66506c04512ed1c600d4c45731088db9c709d Mon Sep 17 00:00:00 2001 From: purhan Date: Fri, 11 Jun 2021 12:03:24 +0530 Subject: [PATCH 03/20] [chores] Fix failing tests --- netengine/backends/snmp/airos.py | 4 ++-- netengine/backends/snmp/base.py | 2 +- netengine/backends/snmp/openwrt.py | 13 +++++++------ tests/static/test-openwrt-snmp-oid.json | 5 ++++- tests/test_snmp/test_openwrt.py | 3 +++ tests/utils.py | 8 +++++++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index fce0844..e194c5d 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -401,9 +401,9 @@ def to_dict(self): return self._dict( { 'type': 'DeviceMonitoring', - 'general': {'uptime': self.uptime,}, + 'general': {'uptime': self.uptime}, 'resources': { - 'memory': {'total': self.RAM_total, 'free': self.RAM_free,}, + 'memory': {'total': self.RAM_total, 'free': self.RAM_free}, }, 'interfaces': self.interfaces_to_dict, } diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index 0dbc129..c80f440 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -51,7 +51,7 @@ 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 '': + if mac_address != '': mac_address = ':'.join( [mac_address[slice(i, i + 2)] for i in range(0, 12, 2) if i != ''] ) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 557d5b7..8de89dd 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -11,6 +11,7 @@ from datetime import timedelta import pytz +from netaddr import EUI, mac_unix_expanded from netengine.backends.snmp import SNMP @@ -480,7 +481,7 @@ def resources_to_dict(self): 'free': self.RAM_free, 'cached': self.RAM_cached, }, - 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free,}, + 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free}, } ) return result @@ -505,14 +506,14 @@ def neighbors(self): 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] + for index, neighbor in enumerate(neighbors): + mac = EUI(int(neighbor[0][1].prettyPrint(), 16), dialect=mac_unix_expanded) + interface_num = neighbor[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])) + state = states_map[str(neighbor_states[index][0][1])] result.append( self._dict( - {'mac': str(mac), 'state': str(state), 'interface': str(interface),} + {'mac': str(mac), 'state': str(state), 'interface': str(interface)} ) ) return result diff --git a/tests/static/test-openwrt-snmp-oid.json b/tests/static/test-openwrt-snmp-oid.json index 94da882..480de5c 100644 --- a/tests/static/test-openwrt-snmp-oid.json +++ b/tests/static/test-openwrt-snmp-oid.json @@ -51,5 +51,8 @@ "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" + "1.3.6.1.2.1.25.1.2.0": { + "type": "bytes", + "value": "\\x07\\xe5\\x06\\x0b\\x06\\x00\r\\x00+\\x00\\x00" + } } diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index 10cfe1e..40c1725 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -116,6 +116,9 @@ def test_CPU_count(self): def test_neighbors(self): self.assertIsInstance(self.device.neighbors, list) + def test_local_time(self): + self.assertIsInstance(self.device.local_time, int) + def test_to_dict(self): with self.nextcmd_patcher as p: SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, []]) diff --git a/tests/utils.py b/tests/utils.py index aca6018..e6fcac0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import codecs import json import os from unittest import mock @@ -32,7 +33,12 @@ def _load_mock_json(file): def _get_mocked_getcmd(data, input): oid = input[2] result = data[oid] - if type(result) == list: + if type(result) == dict: + _type = result['type'] + _value = result['value'] + if _type == 'bytes': + result = codecs.escape_decode(_value)[0] + elif type(result) == list: result = '\n'.join(result[0:]) return [0, 0, 0, [[0, result]]] From c9962e2821cb2ce4b0c8fe8c27e0b8161eddc9ba Mon Sep 17 00:00:00 2001 From: purhan Date: Sun, 13 Jun 2021 13:56:18 +0530 Subject: [PATCH 04/20] [chores/qa] Improvements to tests - Fix failing tests - Pass more realistic values in mocks - Remove redundant code --- netengine/backends/snmp/openwrt.py | 13 +++-- tests/static/test-openwrt-snmp-oid.json | 3 +- tests/test_snmp/test_airos.py | 66 +++++++------------------ tests/test_snmp/test_openwrt.py | 54 ++++++++------------ tests/utils.py | 44 ++++++++++++----- 5 files changed, 81 insertions(+), 99 deletions(-) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 8de89dd..7254125 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -507,10 +507,15 @@ def neighbors(self): result = [] for index, neighbor in enumerate(neighbors): - mac = EUI(int(neighbor[0][1].prettyPrint(), 16), dialect=mac_unix_expanded) - interface_num = neighbor[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[index][0][1])] + try: + mac = EUI( + int(neighbor[0][1].prettyPrint(), 16), dialect=mac_unix_expanded + ) + interface_num = neighbor[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[index][0][1])] + except (IndexError, TypeError): + continue result.append( self._dict( {'mac': str(mac), 'state': str(state), 'interface': str(interface)} diff --git a/tests/static/test-openwrt-snmp-oid.json b/tests/static/test-openwrt-snmp-oid.json index 480de5c..f82cd10 100644 --- a/tests/static/test-openwrt-snmp-oid.json +++ b/tests/static/test-openwrt-snmp-oid.json @@ -54,5 +54,6 @@ "1.3.6.1.2.1.25.1.2.0": { "type": "bytes", "value": "\\x07\\xe5\\x06\\x0b\\x06\\x00\r\\x00+\\x00\\x00" - } + }, + "1.3.6.1.2.1.31.1.1.1.1.5": "br-lan" } diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index ab19a75..a9b5315 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -29,7 +29,7 @@ def setUp(self): target=cmdgen.CommandGenerator, attribute='nextCmd', wrap_obj=self.device._command, - return_value=[0, 0, 0, [[[0, 1]]] * 5], + side_effect=self._get_mocked_nextcmd, ) self.getcmd_patcher = SpyMock._patch( target=cmdgen.CommandGenerator, @@ -40,6 +40,7 @@ def setUp(self): ), ) self.getcmd_patcher.start() + self.nextcmd_patcher.start() def test_get_value_error(self): self.getcmd_patcher.stop() @@ -81,53 +82,37 @@ def test_os(self): self.assertIsInstance(self.device.os, tuple) def test_get_interfaces(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.get_interfaces(), list) + self.assertIsInstance(self.device.get_interfaces(), list) def test_get_interfaces_mtu(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_mtu, list) + self.assertIsInstance(self.device.interfaces_mtu, list) def test_interfaces_state(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_state, list) + self.assertIsInstance(self.device.interfaces_state, list) def test_interfaces_speed(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_speed, list) + self.assertIsInstance(self.device.interfaces_speed, list) def test_interfaces_bytes(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_bytes, list) + self.assertIsInstance(self.device.interfaces_bytes, list) def test_interfaces_MAC(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_MAC, list) + self.assertIsInstance(self.device.interfaces_MAC, list) def test_interfaces_type(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_type, list) + self.assertIsInstance(self.device.interfaces_type, list) def test_interfaces_to_dict(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_to_dict, list) + self.assertIsInstance(self.device.interfaces_to_dict, list) def test_wireless_dbm(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.wireless_dbm, list) + self.assertIsInstance(self.device.wireless_dbm, list) def test_interfaces_number(self): self.assertIsInstance(self.device.interfaces_number, int) def test_wireless_to_dict(self): - with self.nextcmd_patcher as np: - SpyMock._update_patch( - np, - _mock_side_effect=lambda *args: self._get_mocked_wireless_links( - data=args - ), - ) - self.assertIsInstance(self.device.wireless_links, list) + self.assertIsInstance(self.device.wireless_links, list) def test_RAM_free(self): self.assertIsInstance(self.device.RAM_free, int) @@ -136,31 +121,16 @@ def test_RAM_total(self): self.assertIsInstance(self.device.RAM_total, int) def test_to_dict(self): - with self.nextcmd_patcher as np: - SpyMock._update_patch( - np, - _mock_side_effect=lambda *args: self._get_mocked_wireless_links( - data=args - ), - ) - self.assertTrue(isinstance(self.device.to_dict(), dict)) + self.assertTrue(isinstance(self.device.to_dict(), dict)) def test_netjson_compliance(self): - with self.nextcmd_patcher as np: - SpyMock._update_patch( - np, - _mock_side_effect=lambda *args: self._get_mocked_wireless_links( - data=args - ), - ) - device_dict = self.device.to_dict() - device_json = self.device.to_json() - validate(instance=device_dict, schema=schema) - validate(instance=json.loads(device_json), schema=schema) + device_dict = self.device.to_dict() + device_json = self.device.to_json() + validate(instance=device_dict, schema=schema) + validate(instance=json.loads(device_json), schema=schema) def test_manufacturer(self): - with self.nextcmd_patcher: - self.assertIsNotNone(self.device.manufacturer) + self.assertIsNotNone(self.device.manufacturer) def test_model(self): self.assertIsInstance(self.device.model, str) diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index 40c1725..906be6c 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -27,7 +27,7 @@ def setUp(self): target=cmdgen.CommandGenerator, attribute='nextCmd', wrap_obj=self.device._command, - return_value=[0, 0, 0, [[[0, 1]]] * 5], + side_effect=self._get_mocked_nextcmd, ) self.getcmd_patcher = SpyMock._patch( target=cmdgen.CommandGenerator, @@ -38,13 +38,13 @@ def setUp(self): ), ) self.getcmd_patcher.start() + self.nextcmd_patcher.start() def test_os(self): self.assertIsInstance(self.device.os, tuple) def test_manufacturer(self): - with self.nextcmd_patcher: - self.assertIsNotNone(self.device.manufacturer) + self.assertIsNotNone(self.device.manufacturer) def test_name(self): self.assertIsInstance(self.device.name, str) @@ -56,41 +56,31 @@ def test_uptime_tuple(self): self.assertIsInstance(self.device.uptime_tuple, tuple) def test_get_interfaces(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.get_interfaces(), list) + self.assertIsInstance(self.device.get_interfaces(), list) def test_interfaces_speed(self): self.assertIsInstance(self.device.interfaces_speed, list) def test_interfaces_bytes(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_bytes, list) + self.assertIsInstance(self.device.interfaces_bytes, list) def test_interfaces_MAC(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_MAC, list) + self.assertIsInstance(self.device.interfaces_MAC, list) def test_interfaces_type(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_type, list) + self.assertIsInstance(self.device.interfaces_type, list) def test_interfaces_mtu(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_mtu, list) + self.assertIsInstance(self.device.interfaces_mtu, list) def test_interfaces_state(self): - with self.nextcmd_patcher: - self.assertIsInstance(self.device.interfaces_up, list) + self.assertIsInstance(self.device.interfaces_up, list) def test_interfaces_to_dict(self): - with self.nextcmd_patcher as p: - p.return_value = (0, 0, 0, []) - self.assertIsInstance(self.device.interfaces_to_dict, list) + self.assertIsInstance(self.device.interfaces_to_dict, list) def test_interface_addr_and_mask(self): - with self.nextcmd_patcher as p: - p.return_value = (0, 0, 0, []) - self.assertIsInstance(self.device.interface_addr_and_mask, dict) + self.assertIsInstance(self.device.interface_addr_and_mask, dict) def test_RAM_total(self): self.assertIsInstance(self.device.RAM_total, int) @@ -120,21 +110,17 @@ def test_local_time(self): self.assertIsInstance(self.device.local_time, int) 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.assertIsInstance(device_dict, dict) - self.assertEqual( - len(device_dict['interfaces']), len(self.device.get_interfaces()), - ) + device_dict = self.device.to_dict() + self.assertIsInstance(device_dict, dict) + self.assertEqual( + len(device_dict['interfaces']), len(self.device.get_interfaces()), + ) def test_netjson_compliance(self): - with self.nextcmd_patcher as p: - SpyMock._update_patch(p, _mock_return_value=[0, 0, 0, []]) - device_dict = self.device.to_dict() - device_json = self.device.to_json() - validate(instance=device_dict, schema=schema) - validate(instance=json.loads(device_json), schema=schema) + device_dict = self.device.to_dict() + device_json = self.device.to_json() + validate(instance=device_dict, schema=schema) + validate(instance=json.loads(device_json), schema=schema) def tearDown(self): patch.stopall() diff --git a/tests/utils.py b/tests/utils.py index e6fcac0..db1a7f2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,9 +3,19 @@ import os from unittest import mock +from pysnmp.hlapi import OctetString + from .settings import settings +class MockOid: + def getOid(self): + return self.oid + + def __init__(self, oid): + self.oid = oid + + class SpyMock: @staticmethod def _patch(*args, **kwargs): @@ -43,17 +53,27 @@ def _get_mocked_getcmd(data, input): return [0, 0, 0, [[0, result]]] @staticmethod - def _get_mocked_wireless_links(data): - oid = data[2] - return_data = { - '1.3.6.1.4.1.14988.1.1.1.2.1': [0, 0, 0, [[[0, 0], 0]] * 28], - '1.3.6.1.4.1.14988.1.1.1.2.1.3': [0, 0, 0, [0, 0]], - '1.3.6.1.4.1.14988.1.1.1.2.1.3.0': [None, 0, 0, []], - '1.3.6.1.2.1.1.9.1.1': [ - 0, - 0, - 0, - [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + def _get_mocked_nextcmd(*args, **kwargs): + def _get_nextcmd_list(return_value): + # pass `None` as the data we don't use + return [None, None, None, return_value] + + res = { + '1.3.6.1.4.1.14988.1.1.1.2.1': [[[0, 0], 0]] * 28, + '1.3.6.1.4.1.14988.1.1.1.2.1.3': [0, 0], + '1.3.6.1.4.1.14988.1.1.1.2.1.3.0': [], + '1.3.6.1.2.1.1.9.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + '1.3.6.1.2.1.4.35.1.4': [ + MockOid('1.3.6.1.2.5'), + OctetString('0x040e3cca555f'), ], + '1.3.6.1.2.1.4.35.1.7': [MockOid('1.3.6.1.2.5'), 1], + '1.3.6.1.2.1.2.2.1.6.': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + '1.3.6.1.2.1.2.2.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + '1.3.6.1.2.1.4.20.1.1': [[[0, OctetString('127.0.0.1')]]], + '1.3.6.1.2.1.4.20.1.2': [[[0, 1]]], + '1.3.6.1.2.1.25.3.3.1.2': [0, 2], + '1.3.6.1.2.1.4.20.1.3': [[[0, OctetString('192.168.0.1')]]], } - return return_data[oid] + oid = args[2] + return _get_nextcmd_list(res[oid]) From 8cd70894336cf7da49219aacd1b53dd6dd22b840 Mon Sep 17 00:00:00 2001 From: purhan Date: Tue, 15 Jun 2021 15:16:07 +0530 Subject: [PATCH 05/20] [change] Add monitoring info to AirOS backend --- netengine/backends/snmp/airos.py | 91 ++++++++++++++++++++++++++----- tests/static/test-airos-snmp.json | 10 +++- tests/test_snmp/test_airos.py | 25 ++++++++- 3 files changed, 108 insertions(+), 18 deletions(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index e194c5d..17aa7e1 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -7,7 +7,8 @@ import binascii import logging -from datetime import timedelta +import time +from datetime import datetime from .base import SNMP @@ -98,15 +99,6 @@ def uptime(self): """ return int(self.get_value('1.3.6.1.2.1.1.3.0')) // 100 - @property - def uptime_tuple(self): - """ - returns (days, hours, minutes) - """ - td = timedelta(seconds=self.uptime) - - return td.days, td.seconds // 3600, (td.seconds // 60) % 60 - @property def interfaces_number(self): """ @@ -381,6 +373,17 @@ def wireless_links(self): final.append(result) return final + @property + def local_time(self): + """ + returns the local time of the host device as a timestamp + """ + epoch = str(self.get('1.3.6.1.4.1.41112.1.4.8.1.0')[3][0][1]) + timestamp = int( + time.mktime(datetime.strptime(epoch, '%Y-%m-%d %H:%M:%S').timetuple()) + ) + return timestamp + @property def RAM_total(self): """ @@ -397,14 +400,74 @@ def RAM_free(self): free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0') return int(free) + @property + def RAM_buffered(self): + """ + Returns the buffered RAM of the device + """ + buffered = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.3.0') + return int(buffered) + + @property + def RAM_cached(self): + """ + Returns the cached RAM of the device + """ + cached = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.4.0') + return int(cached) + + @property + def load(self): + """ + Returns an array with load average values respectively in the last + minute, in the last 5 minutes and in the last 15 minutes + """ + one = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.1')) + five = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.2')) + fifteen = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.3')) + return [one, five, fifteen] + + @property + def SWAP_total(self): + """ + Returns the total SWAP of the device + """ + total = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.1.0') + return int(total) + + @property + def SWAP_free(self): + """ + Returns the free SWAP of the device + """ + free = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.2.0') + return int(free) + + @property + def resources_to_dict(self): + """ + returns an ordered dict with hardware resources information + """ + result = self._dict( + { + 'load': self.load, + 'memory': { + 'total': self.RAM_total, + 'buffered': self.RAM_buffered, + 'free': self.RAM_free, + 'cached': self.RAM_cached, + }, + 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free}, + } + ) + return result + def to_dict(self): return self._dict( { 'type': 'DeviceMonitoring', - 'general': {'uptime': self.uptime}, - 'resources': { - 'memory': {'total': self.RAM_total, 'free': self.RAM_free}, - }, + 'general': {'uptime': self.uptime, 'local_time': self.local_time}, + 'resources': self.resources_to_dict, 'interfaces': self.interfaces_to_dict, } ) diff --git a/tests/static/test-airos-snmp.json b/tests/static/test-airos-snmp.json index 6890337..2f6f0ec 100644 --- a/tests/static/test-airos-snmp.json +++ b/tests/static/test-airos-snmp.json @@ -46,5 +46,13 @@ "1.3.6.1.2.1.2.2.1.5.2": "0", "1.3.6.1.2.1.2.2.1.5.3": "0", "1.3.6.1.2.1.2.2.1.5.4": "0", - "1.3.6.1.2.1.2.2.1.5.5": "0" + "1.3.6.1.2.1.2.2.1.5.5": "0", + "1.3.6.1.4.1.41112.1.4.8.1.0": "2020-02-03 13:01:14", + "1.3.6.1.4.1.10002.1.1.1.1.3.0": "2648", + "1.3.6.1.4.1.10002.1.1.1.1.4.0": "0", + "1.3.6.1.4.1.10002.1.1.1.4.2.1.3.1": "64", + "1.3.6.1.4.1.10002.1.1.1.4.2.1.3.2": "69", + "1.3.6.1.4.1.10002.1.1.1.4.2.1.3.3": "32", + "1.3.6.1.4.1.10002.1.1.1.2.1.0": "0", + "1.3.6.1.4.1.10002.1.1.1.2.2.0": "0" } diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index a9b5315..70ee9e0 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -73,7 +73,6 @@ def test_properties(self): device.model device.os device.uptime - device.uptime_tuple def test_name(self): self.assertIsInstance(self.device.name, str) @@ -141,8 +140,28 @@ def test_firmware(self): def test_uptime(self): self.assertIsInstance(self.device.uptime, int) - def test_uptime_tuple(self): - self.assertIsInstance(self.device.uptime_tuple, tuple) + def test_RAM_buffered(self): + self.assertIsInstance(self.device.RAM_buffered, int) + + def test_RAM_cached(self): + self.assertIsInstance(self.device.RAM_cached, 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_local_time(self): + self.assertIsInstance(self.device.local_time, int) + + def test_load(self): + load = self.device.load + self.assertIsInstance(load, list) + self.assertEquals(len(load), 3) + self.assertIsInstance(load[0], int) + self.assertIsInstance(load[1], int) + self.assertIsInstance(load[2], int) def tearDown(self): patch.stopall() From da399656c06aa013f76fc9f88c957b3ccd29ea80 Mon Sep 17 00:00:00 2001 From: purhan Date: Fri, 18 Jun 2021 11:54:34 +0530 Subject: [PATCH 06/20] [chores] Some requested changes - Swap time with datetime in local_time - Remove unused warning - Use a single call for collecting load information --- netengine/backends/snmp/airos.py | 12 +++++------- netengine/backends/snmp/openwrt.py | 1 - tests/test_snmp/test_airos.py | 2 +- tests/utils.py | 1 + 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 17aa7e1..78ac856 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -7,7 +7,6 @@ import binascii import logging -import time from datetime import datetime from .base import SNMP @@ -379,9 +378,7 @@ def local_time(self): returns the local time of the host device as a timestamp """ epoch = str(self.get('1.3.6.1.4.1.41112.1.4.8.1.0')[3][0][1]) - timestamp = int( - time.mktime(datetime.strptime(epoch, '%Y-%m-%d %H:%M:%S').timetuple()) - ) + timestamp = int(datetime.strptime(epoch, '%Y-%m-%d %H:%M:%S').timestamp()) return timestamp @property @@ -422,9 +419,10 @@ def load(self): Returns an array with load average values respectively in the last minute, in the last 5 minutes and in the last 15 minutes """ - one = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.1')) - five = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.2')) - fifteen = int(self.get_value('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.3')) + array = (self.next('1.3.6.1.4.1.10002.1.1.1.4.2.1.3'))[3] + one = int(array[0][0][1]) + five = int(array[1][0][1]) + fifteen = int(array[2][0][1]) return [one, five, fifteen] @property diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 7254125..dbf8546 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -412,7 +412,6 @@ def local_time(self): tzinfo=pytz.utc, ).timestamp() ) - logger.warning('Invalid timestring was supplied') @property def RAM_total(self): diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index 70ee9e0..08e7b58 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -158,7 +158,7 @@ def test_local_time(self): def test_load(self): load = self.device.load self.assertIsInstance(load, list) - self.assertEquals(len(load), 3) + self.assertEqual(len(load), 3) self.assertIsInstance(load[0], int) self.assertIsInstance(load[1], int) self.assertIsInstance(load[2], int) diff --git a/tests/utils.py b/tests/utils.py index db1a7f2..04bade5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -74,6 +74,7 @@ def _get_nextcmd_list(return_value): '1.3.6.1.2.1.4.20.1.2': [[[0, 1]]], '1.3.6.1.2.1.25.3.3.1.2': [0, 2], '1.3.6.1.2.1.4.20.1.3': [[[0, OctetString('192.168.0.1')]]], + '1.3.6.1.4.1.10002.1.1.1.4.2.1.3': [[[0, 51]], [[0, 18]], [[0, 24]]], } oid = args[2] return _get_nextcmd_list(res[oid]) From 180033283d783460f1add4a4453a13251bddfe10 Mon Sep 17 00:00:00 2001 From: purhan Date: Fri, 25 Jun 2021 16:25:34 +0530 Subject: [PATCH 07/20] [chores] Fix neighbor information when mac address is not found --- netengine/backends/snmp/openwrt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index dbf8546..2bb8616 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -513,7 +513,7 @@ def neighbors(self): interface_num = neighbor[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[index][0][1])] - except (IndexError, TypeError): + except (IndexError, TypeError, ValueError): continue result.append( self._dict( From cda0295cb18e5d61f383d5d348e61b4d30f59fd3 Mon Sep 17 00:00:00 2001 From: purhan Date: Mon, 5 Jul 2021 22:31:21 +0530 Subject: [PATCH 08/20] [change] Add IP to neighbor information --- netengine/backends/snmp/base.py | 16 ++++++++++++++++ netengine/backends/snmp/openwrt.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index c80f440..72559e5 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -8,6 +8,8 @@ import binascii import logging +import netaddr + from netengine.backends import BaseBackend from netengine.exceptions import NetEngineError @@ -57,6 +59,20 @@ def _octet_to_mac(self, octet_mac): ) return mac_address + def _ascii_blocks_to_ipv6(self, ascii_string): + """ + converts an ascii representation into ipv6 address + """ + blocks = ascii_string.split('.') + for b in range(len(blocks)): + blocks[b] = format(int(blocks[b]), '02x') + res = netaddr.IPAddress( + ':'.join( + [''.join(blocks[slice(i, i + 2)]) for i in range(0, len(blocks), 2)] + ) + ) + return res + 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 2bb8616..02dc3e4 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -507,6 +507,11 @@ def neighbors(self): for index, neighbor in enumerate(neighbors): try: + oid = neighbor[0][0].getOid() + if oid[12] == 4: + ip = oid[13:] + else: + ip = self._ascii_blocks_to_ipv6(str(oid[13:])) mac = EUI( int(neighbor[0][1].prettyPrint(), 16), dialect=mac_unix_expanded ) @@ -517,7 +522,12 @@ def neighbors(self): continue result.append( self._dict( - {'mac': str(mac), 'state': str(state), 'interface': str(interface)} + { + 'mac': str(mac), + 'state': str(state), + 'interface': str(interface), + 'ip': str(ip), + } ) ) return result From 0edc93abbd703078c5060a34f79b31a21b33cecb Mon Sep 17 00:00:00 2001 From: purhan Date: Tue, 6 Jul 2021 19:53:22 +0530 Subject: [PATCH 09/20] [chores] Add comment --- netengine/backends/snmp/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index 72559e5..9caff9d 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -65,7 +65,9 @@ def _ascii_blocks_to_ipv6(self, ascii_string): """ blocks = ascii_string.split('.') for b in range(len(blocks)): + # convert each block of decimal into hexadecimal form without `0x` prefix blocks[b] = format(int(blocks[b]), '02x') + # join the obtained list into a valid IP string res = netaddr.IPAddress( ':'.join( [''.join(blocks[slice(i, i + 2)]) for i in range(0, len(blocks), 2)] From acafe93b3307f0a0a5b12f9c7463327716e61840 Mon Sep 17 00:00:00 2001 From: purhan Date: Tue, 6 Jul 2021 20:19:34 +0530 Subject: [PATCH 10/20] [change] Add more interfaces, Add RAM_used --- netengine/backends/snmp/openwrt.py | 23 +-- netengine/backends/snmp/utils.py | 236 +++++++++++++++++++++++++++++ tests/test_snmp/test_openwrt.py | 3 + 3 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 netengine/backends/snmp/utils.py diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 02dc3e4..2c06ef1 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -15,6 +15,8 @@ from netengine.backends.snmp import SNMP +from .utils import ifTypes + logger = logging.getLogger(__name__) @@ -266,11 +268,6 @@ def interfaces_type(self): Returns an ordered dict with the interface type (e.g Ethernet, loopback) """ if self._interfaces_type is None: - types = { - '6': 'ethernetCsmacd', - '24': 'softwareLoopback', - '131': 'tunnel', - } results = [] starting = '1.3.6.1.2.1.2.2.1.2.' types_oid = '1.3.6.1.2.1.2.2.1.3.' @@ -278,7 +275,9 @@ def interfaces_type(self): result = self._dict( { 'name': self.get_value(starting + str(i)), - 'type': types[self.get_value(types_oid + str(i))], + 'type': ifTypes.get( + self.get_value(types_oid + str(i)), 'unknown' + ), } ) results.append(result) @@ -434,14 +433,19 @@ def RAM_cached(self): """ return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.7')) + @property + def RAM_used(self): + """ + returns the used RAM of the device + """ + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.1')) + @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 + return int(self.RAM_total - (self.RAM_used - self.RAM_cached)) @property def SWAP_total(self): @@ -477,6 +481,7 @@ def resources_to_dict(self): 'memory': { 'total': self.RAM_total, 'shared': self.RAM_shared, + 'used': self.RAM_used, 'free': self.RAM_free, 'cached': self.RAM_cached, }, diff --git a/netengine/backends/snmp/utils.py b/netengine/backends/snmp/utils.py new file mode 100644 index 0000000..aa72815 --- /dev/null +++ b/netengine/backends/snmp/utils.py @@ -0,0 +1,236 @@ +ifTypes = { + '1': 'other', + '2': 'regular1822', + '3': 'hdh1822', + '4': 'ddnX25', + '5': 'rfc877x25', + '6': 'ethernet Csmacd', + '7': 'iso88023 Csmacd', + '8': 'iso88024TokenBus', + '9': 'iso88025TokenRing', + '10': 'iso88026Man', + '11': 'starLan', + '12': 'proteon10Mbit', + '13': 'proteon80Mbit', + '14': 'hyperchannel', + '15': 'fddi', + '16': 'lapb', + '17': 'sdlc', + '18': 'ds1', + '19': 'e1', + '20': 'basic ISDN', + '21': 'primary ISDN', + '22': 'prop PointToPoint Serial', + '23': 'ppp', + '24': 'software loopback', + '25': 'eon', + '26': 'ethernet 3Mbit', + '27': 'nsip', + '28': 'slip', + '29': 'ultra', + '30': 'ds3', + '31': 'sip', + '32': 'frameRelay', + '33': 'rs232', + '34': 'para', + '35': 'arcnet', + '36': 'arcnetPlus', + '37': 'atm', + '38': 'miox25', + '39': 'sonet', + '40': 'x25ple', + '41': 'iso88022llc', + '42': 'localTalk', + '43': 'smdsDxi', + '44': 'frameRelayService', + '45': 'v35', + '46': 'hssi', + '47': 'hippi', + '48': 'modem', + '49': 'aal5', + '50': 'sonetPath', + '51': 'sonetVT', + '52': 'smdsIcip', + '53': 'propVirtual', + '54': 'propMultiplexor', + '55': 'ieee80212', + '56': 'fibreChannel', + '57': 'hippiInterface', + '58': 'frameRelayInterconnect', + '59': 'aflane8023', + '60': 'aflane8025', + '61': 'cctEmul', + '62': 'fastEther', + '63': 'isdn', + '64': 'v11', + '65': 'v36', + '66': 'g703at64k', + '67': 'g703at2mb', + '68': 'qllc', + '69': 'fastEtherFX', + '70': 'channel', + '71': 'ieee 80211', + '72': 'ibm370parChan', + '73': 'escon', + '74': 'dlsw', + '75': 'isdns', + '76': 'isdnu', + '77': 'lapd', + '78': 'ipSwitch', + '79': 'rsrb', + '80': 'atmLogical', + '81': 'ds0', + '82': 'ds0Bundle', + '83': 'bsc', + '84': 'async', + '85': 'cnr', + '86': 'iso88025Dtr', + '87': 'eplrs', + '88': 'arap', + '89': 'propCnls', + '90': 'hostPad', + '91': 'termPad', + '92': 'frameRelayMPI', + '93': 'x213', + '94': 'adsl', + '95': 'radsl', + '96': 'sdsl', + '97': 'vdsl', + '98': 'iso88025CRFPInt', + '99': 'myrinet', + '100': 'voiceEM', + '101': 'voiceFXO', + '102': 'voiceFXS', + '103': 'voiceEncap', + '104': 'voiceOverIp', + '105': 'atmDxi', + '106': 'atmFuni', + '107': 'atmIma', + '108': 'pppMultilinkBundle', + '109': 'ipOverCdlc', + '110': 'ipOverClaw', + '111': 'stackToStack', + '112': 'virtualIpAddress', + '113': 'mpc', + '114': 'ipOverAtm', + '115': 'iso88025Fiber', + '116': 'tdlc', + '117': 'gigabitEthernet', + '118': 'hdlc', + '119': 'lapf', + '120': 'v37', + '121': 'x25mlp', + '122': 'x25huntGroup', + '123': 'trasnpHdlc', + '124': 'interleave', + '125': 'fast', + '126': 'ip', + '127': 'docsCableMaclayer', + '128': 'docsCableDownstream', + '129': 'docsCableUpstream', + '130': 'a12MppSwitch', + '131': 'tunnel', + '132': 'coffee', + '133': 'ces', + '134': 'atmSubInterface', + '135': 'l2vlan', + '136': 'l3ipvlan', + '137': 'l3ipxvlan', + '138': 'digitalPowerline', + '139': 'mediaMailOverIp', + '140': 'dtm', + '141': 'dcn', + '142': 'ipForward', + '143': 'msdsl', + '144': 'ieee1394', + '145': 'if-gsn', + '146': 'dvbRccMacLayer', + '147': 'dvbRccDownstream', + '148': 'dvbRccUpstream', + '149': 'atmVirtual', + '150': 'mplsTunnel', + '151': 'srp', + '152': 'voiceOverAtm', + '153': 'voiceOverFrameRelay', + '154': 'idsl', + '155': 'compositeLink', + '156': 'ss7SigLink', + '157': 'propWirelessP2P', + '158': 'frForward', + '159': 'rfc1483', + '160': 'usb', + '161': '802.3ad link aggregation', + '162': 'bgp policy accounting', + '163': 'frf16MfrBundle', + '164': 'h323Gatekeeper', + '165': 'h323Proxy', + '166': 'mpls', + '167': 'mfSigLink', + '168': 'hdsl2', + '169': 'shdsl', + '170': 'ds1FDL', + '171': 'pos', + '172': 'dvbAsiIn', + '173': 'dvbAsiOut', + '174': 'plc', + '175': 'nfas', + '176': 'tr008', + '177': 'gr303RDT', + '178': 'gr303IDT', + '179': 'isup', + '180': 'propDocsWirelessMaclayer', + '181': 'propDocsWirelessDownstream', + '182': 'propDocsWirelessUpstream', + '183': 'hiperlan2', + '184': 'propBWAp2Mp', + '185': 'sonetOverheadChannel', + '186': 'digitalWrapperOverheadChannel', + '187': 'aal2', + '188': 'radioMAC', + '189': 'atmRadio', + '190': 'imt', + '191': 'mvl', + '192': 'reachDSL', + '193': 'frDlciEndPt', + '194': 'atmVciEndPt', + '195': 'opticalChannel', + '196': 'opticalTransport', + '197': 'propAtm', + '198': 'voiceOverCable', + '199': 'infiniband', + '200': 'teLink', + '201': 'q2931', + '202': 'virtualTg', + '203': 'sipTg', + '204': 'sipSig', + '205': 'docsCableUpstreamChannel', + '206': 'econet', + '207': 'pon155', + '208': 'pon622', + '209': 'bridge', + '210': 'linegroup', + '211': 'voiceEMFGD', + '212': 'voiceFGDEANA', + '213': 'voiceDID', + '214': 'mpegTransport', + '215': 'sixToFour', + '216': 'gtp', + '217': 'pdnEtherLoop1', + '218': 'pdnEtherLoop2', + '219': 'opticalChannelGroup', + '220': 'homepna', + '221': 'gfp', + '222': 'ciscoISLvlan', + '223': 'actelisMetaLOOP', + '224': 'fcipLink', + '225': 'rpr', + '226': 'qam', + '227': 'lmp', + '228': 'cblVectaStar', + '229': 'docsCableMCmtsDownstream', + '230': 'adsl2', + '231': 'macSecControlledIF', + '232': 'macSecUncontrolledIF', + '233': 'aviciOpticalEther', + '234': 'atmbond', +} diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index 906be6c..e81e2f8 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -91,6 +91,9 @@ def test_RAM_shared(self): def test_RAM_cached(self): self.assertIsInstance(self.device.RAM_cached, int) + def test_RAM_used(self): + self.assertIsInstance(self.device.RAM_used, int) + def test_RAM_free(self): self.assertIsInstance(self.device.RAM_free, int) From 34f05c9d709ced7a62df6a4d62abe037ef7cbffc Mon Sep 17 00:00:00 2001 From: purhan Date: Wed, 7 Jul 2021 19:50:10 +0530 Subject: [PATCH 11/20] [change] Use one query for neighbors and their states --- netengine/backends/snmp/openwrt.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 2c06ef1..20808f1 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -505,11 +505,19 @@ def neighbors(self): '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] + neighbors_oid = '1.3.6.1.2.1.4.35.1.4' + neighbor_states_oid = '1.3.6.1.2.1.4.35.1.7' + neighbor_info = self.next('1.3.6.1.2.1.4.35.1')[3] + neighbors = [] + neighbor_states = [] result = [] + for oid in neighbor_info: + if neighbors_oid in str(oid[0][0]): + neighbors.append(oid) + elif neighbor_states_oid in str(oid[0][0]): + neighbor_states.append(oid) + for index, neighbor in enumerate(neighbors): try: oid = neighbor[0][0].getOid() From 034475ca65673c95242b4e993b81dae84497292a Mon Sep 17 00:00:00 2001 From: purhan Date: Mon, 12 Jul 2021 22:53:30 +0530 Subject: [PATCH 12/20] [chores] Fix failing build --- tests/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 04bade5..0e1e2bb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -63,11 +63,6 @@ def _get_nextcmd_list(return_value): '1.3.6.1.4.1.14988.1.1.1.2.1.3': [0, 0], '1.3.6.1.4.1.14988.1.1.1.2.1.3.0': [], '1.3.6.1.2.1.1.9.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], - '1.3.6.1.2.1.4.35.1.4': [ - MockOid('1.3.6.1.2.5'), - OctetString('0x040e3cca555f'), - ], - '1.3.6.1.2.1.4.35.1.7': [MockOid('1.3.6.1.2.5'), 1], '1.3.6.1.2.1.2.2.1.6.': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], '1.3.6.1.2.1.2.2.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], '1.3.6.1.2.1.4.20.1.1': [[[0, OctetString('127.0.0.1')]]], @@ -75,6 +70,10 @@ def _get_nextcmd_list(return_value): '1.3.6.1.2.1.25.3.3.1.2': [0, 2], '1.3.6.1.2.1.4.20.1.3': [[[0, OctetString('192.168.0.1')]]], '1.3.6.1.4.1.10002.1.1.1.4.2.1.3': [[[0, 51]], [[0, 18]], [[0, 24]]], + '1.3.6.1.2.1.4.35.1': [ + [[[MockOid('1.3.6.1.2.1.4.35.1.4')]], OctetString('0x040e3cca555f')], + [[[MockOid('1.3.6.1.2.1.4.35.1.7')]], 1], + ], } oid = args[2] return _get_nextcmd_list(res[oid]) From 995b7034fe10a872dfa407654568072d4f9b3733 Mon Sep 17 00:00:00 2001 From: purhan Date: Fri, 16 Jul 2021 21:59:58 +0530 Subject: [PATCH 13/20] [change] Implement SNMPDUMP --- netengine/backends/base.py | 8 +- netengine/backends/dummy.py | 2 +- netengine/backends/snmp/airos.py | 242 ++++++++++++++------------- netengine/backends/snmp/base.py | 40 ++++- netengine/backends/snmp/openwrt.py | 258 ++++++++++++++++------------- requirements.txt | 1 + tests/test_dummy.py | 2 +- tests/test_snmp/test_airos.py | 67 ++++---- tests/test_snmp/test_openwrt.py | 52 +++--- tests/utils.py | 22 +-- 10 files changed, 378 insertions(+), 316 deletions(-) diff --git a/netengine/backends/base.py b/netengine/backends/base.py index 86509b0..fdd414d 100644 --- a/netengine/backends/base.py +++ b/netengine/backends/base.py @@ -21,14 +21,14 @@ def __repr__(self): """returns unicode string represantation""" return self.__str__() - def validate(self): + def validate(self, *args, **kwargs): raise NotImplementedError('Not implemented') - def to_dict(self): + def to_dict(self, autowalk=True): raise NotImplementedError('Not implemented') - def to_json(self, **kwargs): - dictionary = self.to_dict() + def to_json(self, autowalk=True, **kwargs): + dictionary = self.to_dict(autowalk=autowalk) return json.dumps(dictionary, **kwargs) @property diff --git a/netengine/backends/dummy.py b/netengine/backends/dummy.py index 2173234..5db8597 100644 --- a/netengine/backends/dummy.py +++ b/netengine/backends/dummy.py @@ -63,7 +63,7 @@ def get_interfaces(self): }, ] - def to_dict(self): + def to_dict(self, *args, **kwargs): return self._dict( { 'name': 'dummy', diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 78ac856..5145439 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -9,6 +9,8 @@ import logging from datetime import datetime +from pytrie import StringTrie as Trie + from .base import SNMP logger = logging.getLogger(__name__) @@ -19,56 +21,54 @@ class AirOS(SNMP): Ubiquiti AirOS SNMP backend """ - _oid_to_retrieve = '1.3.6.1.2.1.1.9.1.1' + _oid_to_retrieve = '1.3.6.1.2.1.1.9.1.1.' - def __str__(self): + def __str__(self, snmpdump=None): """print a human readable object description""" return f'' - def validate(self): + def validate(self, snmpdump=None): """ raises NetEngineError exception if anything is wrong with the connection for example: wrong host, invalid community """ # this triggers a connection which # will raise an exception if anything is wrong - return self.name + return self.name(snmpdump=snmpdump) - @property - def os(self): + def os(self, snmpdump=None): """ returns (os_name, os_version) """ os_name = 'AirOS' - os_version = self.get_value('1.3.6.1.2.1.1.1.0').split('#')[0].strip() + os_version = ( + self.get_value('1.3.6.1.2.1.1.1.0', snmpdump=snmpdump).split('#')[0].strip() + ) return os_name, os_version - @property - def name(self): + def name(self, snmpdump=None): """ returns a string containing the device name """ - return self.get_value('1.3.6.1.2.1.1.5.0') + return self.get_value('1.3.6.1.2.1.1.5.0', snmpdump=snmpdump) - @property - def model(self): + def model(self, snmpdump=None): """ returns a string containing the device model """ oids = ['1.2.840.10036.3.1.2.1.3.5', '1.2.840.10036.3.1.2.1.3.8'] for oid in oids: - model = self.get_value(oid) + model = self.get_value(oid, snmpdump=snmpdump) if model != '': return model - @property - def firmware(self): + def firmware(self, snmpdump=None): """ returns a string containing the device firmware """ oids = ['1.2.840.10036.3.1.2.1.4.5', '1.2.840.10036.3.1.2.1.4.8'] for oid in oids: - tmp = self.get_value(oid).split('.') + tmp = self.get_value(oid, snmpdump=snmpdump).split('.') if tmp is not None: length = len(tmp) i = 0 @@ -77,37 +77,35 @@ def firmware(self): return 'AirOS ' + '.'.join(tmp[i:length]) i = i + 1 - @property - def manufacturer(self): - return self.get_manufacturer(self.interfaces_MAC[1]['mac_address']) + def manufacturer(self, snmpdump=None): + return self.get_manufacturer( + self.interfaces_MAC(snmpdump=snmpdump)[1]['mac_address'] + ) - @property - def ssid(self): + def ssid(self, snmpdump=None): """ returns a string containing the wireless ssid """ oids = ['1.2.840.10036.1.1.1.9.5', '1.2.840.10036.1.1.1.9.8'] for oid in oids: - if self.get_value(oid) != '': - return self.get_value(oid) + if self.get_value(oid, snmpdump=snmpdump) != '': + return self.get_value(oid, snmpdump=snmpdump) - @property - def uptime(self): + def uptime(self, snmpdump=None): """ returns an integer representing the number of seconds of uptime """ - return int(self.get_value('1.3.6.1.2.1.1.3.0')) // 100 + return int(self.get_value('1.3.6.1.2.1.1.3.0', snmpdump=snmpdump)) // 100 - @property - def interfaces_number(self): + def interfaces_number(self, snmpdump=None): """ Returns the number of the network interfaces """ - return int(self.get_value('1.3.6.1.2.1.2.1.0')) + return int(self.get_value('1.3.6.1.2.1.2.1.0', snmpdump=snmpdump)) _interfaces = None - def get_interfaces(self): + def get_interfaces(self, snmpdump=None): """ returns the list of all the interfaces of the device """ @@ -115,10 +113,10 @@ def get_interfaces(self): interfaces = [] value_to_get = '1.3.6.1.2.1.2.2.1.2.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): value_to_get1 = value_to_get + str(i) if value_to_get1: - interfaces.append(self.get_value(value_to_get1)) + interfaces.append(self.get_value(value_to_get1, snmpdump=snmpdump)) self._interfaces = interfaces @@ -126,8 +124,7 @@ def get_interfaces(self): _interfaces_mtu = None - @property - def interfaces_mtu(self): + def interfaces_mtu(self, snmpdump=None): """ Returns an ordereed dict with the interface and its MTU """ @@ -138,11 +135,11 @@ def interfaces_mtu(self): tmp[18] = str(4) to = ''.join(tmp) - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'mtu': int(self.get_value(to + str(i))), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'mtu': int(self.get_value(to + str(i), snmpdump=snmpdump)), } ) results.append(result) @@ -153,8 +150,7 @@ def interfaces_mtu(self): _interfaces_state = None - @property - def interfaces_state(self): + def interfaces_state(self, snmpdump=None): """ Returns an ordereed dict with the interfaces and their state (up, down) """ @@ -164,20 +160,27 @@ def interfaces_state(self): operative = '1.3.6.1.2.1.2.2.1.8.' tmp = list(starting) 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: + for i in self._value_to_retrieve(snmpdump=snmpdump): + if self.get_value(starting + str(i), snmpdump=snmpdump) != '': + if int(self.get_value(operative + str(i), snmpdump=snmpdump)) == 1: result = self._dict( - {'name': self.get_value(starting + str(i)), 'state': 'up'} + { + 'name': self.get_value( + starting + str(i), snmpdump=snmpdump + ), + 'state': 'up', + } ) else: result = self._dict( { - 'name': self.get_value(starting + str(i)), + 'name': self.get_value( + starting + str(i), snmpdump=snmpdump + ), 'state': 'down', } ) - elif self.get_value(starting + str(i)) == '': + elif self.get_value(starting + str(i), snmpdump=snmpdump) == '': result = self._dict({'name': '', 'state': ''}) # append result to list results.append(result) @@ -188,8 +191,7 @@ def interfaces_state(self): _interfaces_speed = None - @property - def interfaces_speed(self): + def interfaces_speed(self, snmpdump=None): """ Returns an ordered dict with the interface and ist speed in bps """ @@ -198,11 +200,13 @@ def interfaces_speed(self): starting = '1.3.6.1.2.1.2.2.1.2.' starting_speed = '1.3.6.1.2.1.2.2.1.5.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'speed': int(self.get_value(starting_speed + str(i))), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'speed': int( + self.get_value(starting_speed + str(i), snmpdump=snmpdump) + ), } ) results.append(result) @@ -213,8 +217,7 @@ def interfaces_speed(self): _interfaces_bytes = None - @property - def interfaces_bytes(self): + def interfaces_bytes(self, snmpdump=None): """ Returns an ordereed dict with the interface and its tx and rx octets (1 octet = 1 byte = 8 bits) """ @@ -224,12 +227,16 @@ def interfaces_bytes(self): starting_rx = '1.3.6.1.2.1.2.2.1.10.' starting_tx = '1.3.6.1.2.1.2.2.1.16.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'tx': int(self.get_value(starting_tx + str(i))), - 'rx': int(self.get_value(starting_rx + str(i))), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'tx': int( + self.get_value(starting_tx + str(i), snmpdump=snmpdump) + ), + 'rx': int( + self.get_value(starting_rx + str(i), snmpdump=snmpdump) + ), } ) results.append(result) @@ -239,8 +246,7 @@ def interfaces_bytes(self): _interfaces_MAC = None - @property - def interfaces_MAC(self): + def interfaces_MAC(self, snmpdump=None): """ Returns an ordered dict with the hardware address of every interface """ @@ -249,9 +255,9 @@ def interfaces_MAC(self): starting = '1.3.6.1.2.1.2.2.1.2.' starting_mac = '1.3.6.1.2.1.2.2.1.6.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): mac = binascii.b2a_hex( - self.get_value(starting_mac + str(i)).encode() + self.get_value(starting_mac + str(i), snmpdump=snmpdump).encode() ).decode() # now we are going to format mac as the canonical way as a MAC # address is intended by inserting ':' every two chars of mac @@ -261,7 +267,7 @@ def interfaces_MAC(self): ) result = self._dict( { - 'name': self.get_value(starting + str(i)), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), 'mac_address': mac_transformed, } ) @@ -273,8 +279,7 @@ def interfaces_MAC(self): _interfaces_type = None - @property - def interfaces_type(self): + def interfaces_type(self, snmpdump=None): """ Returns an ordered dict with the interface type (e.g Ethernet, loopback) """ @@ -284,11 +289,13 @@ def interfaces_type(self): starting = '1.3.6.1.2.1.2.2.1.2.' types_oid = '1.3.6.1.2.1.2.2.1.3.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'type': types[self.get_value(types_oid + str(i))], + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'type': types[ + self.get_value(types_oid + str(i), snmpdump=snmpdump) + ], } ) results.append(result) @@ -297,45 +304,48 @@ def interfaces_type(self): return self._interfaces_type - @property - def interfaces_to_dict(self): + def interfaces_to_dict(self, snmpdump=None): """ Returns an ordered dict with all the information available about the interface """ results = [] - for i in range(0, len(self.get_interfaces())): + for i in range(0, len(self.get_interfaces(snmpdump=snmpdump))): logger.info(f'===== {i} =====') result = self._dict( { - 'name': self.interfaces_MAC[i]['name'], + 'name': self.interfaces_MAC(snmpdump=snmpdump)[i]['name'], 'statistics': { - 'rx_bytes': int(self.interfaces_bytes[i]['rx']), - 'tx_bytes': int(self.interfaces_bytes[i]['tx']), + 'rx_bytes': int( + self.interfaces_bytes(snmpdump=snmpdump)[i]['rx'] + ), + 'tx_bytes': int( + self.interfaces_bytes(snmpdump=snmpdump)[i]['tx'] + ), }, } ) results.append(result) return results - @property - def wireless_dbm(self): + def wireless_dbm(self, snmpdump=None): """ returns a list with the wireless signal (dbm) of the link/s """ - res = self.next('1.3.6.1.4.1.14988.1.1.1.2.1.3.0') + res = self.next('1.3.6.1.4.1.14988.1.1.1.2.1.3.0.', snmpdump=snmpdump) dbm = [] for i in range(0, len(res[3])): dbm.append(int(res[3][i][0][1])) return dbm - @property - def wireless_links(self): + def wireless_links(self, snmpdump=None): ''' Returns an ordered dict with all the infos about the wireless link/s ''' final = [] - results = self.next('1.3.6.1.4.1.14988.1.1.1.2.1') - link_number = len(self.next('1.3.6.1.4.1.14988.1.1.1.2.1.3')[3]) + results = self.next('1.3.6.1.4.1.14988.1.1.1.2.1.', snmpdump=snmpdump) + link_number = len( + self.next('1.3.6.1.4.1.14988.1.1.1.2.1.3.', snmpdump=snmpdump)[3] + ) separated_by_meaning = [] dbm = [] tx_bytes = [] @@ -372,100 +382,100 @@ def wireless_links(self): final.append(result) return final - @property - def local_time(self): + def local_time(self, snmpdump=None): """ returns the local time of the host device as a timestamp """ - epoch = str(self.get('1.3.6.1.4.1.41112.1.4.8.1.0')[3][0][1]) + epoch = str(self.get('1.3.6.1.4.1.41112.1.4.8.1.0', snmpdump=snmpdump)[3][0][1]) timestamp = int(datetime.strptime(epoch, '%Y-%m-%d %H:%M:%S').timestamp()) return timestamp - @property - def RAM_total(self): + def RAM_total(self, snmpdump=None): """ Returns the total RAM of the device """ - total = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.1.0') + total = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.1.0', snmpdump=snmpdump) return int(total) - @property - def RAM_free(self): + def RAM_free(self, snmpdump=None): """ Returns the free RAM of the device """ - free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0') + free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0', snmpdump=snmpdump) return int(free) - @property - def RAM_buffered(self): + def RAM_buffered(self, snmpdump=None): """ Returns the buffered RAM of the device """ - buffered = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.3.0') + buffered = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.3.0', snmpdump=snmpdump) return int(buffered) - @property - def RAM_cached(self): + def RAM_cached(self, snmpdump=None): """ Returns the cached RAM of the device """ - cached = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.4.0') + cached = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.4.0', snmpdump=snmpdump) return int(cached) - @property - def load(self): + def load(self, snmpdump=None): """ Returns an array with load average values respectively in the last minute, in the last 5 minutes and in the last 15 minutes """ - array = (self.next('1.3.6.1.4.1.10002.1.1.1.4.2.1.3'))[3] + array = self.next('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.', snmpdump=snmpdump)[3] one = int(array[0][0][1]) five = int(array[1][0][1]) fifteen = int(array[2][0][1]) return [one, five, fifteen] - @property - def SWAP_total(self): + def SWAP_total(self, snmpdump=None): """ Returns the total SWAP of the device """ - total = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.1.0') + total = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.1.0', snmpdump=snmpdump) return int(total) - @property - def SWAP_free(self): + def SWAP_free(self, snmpdump=None): """ Returns the free SWAP of the device """ - free = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.2.0') + free = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.2.0', snmpdump=snmpdump) return int(free) - @property - def resources_to_dict(self): + def resources_to_dict(self, snmpdump=None): """ returns an ordered dict with hardware resources information """ result = self._dict( { - 'load': self.load, + 'load': self.load(snmpdump=snmpdump), 'memory': { - 'total': self.RAM_total, - 'buffered': self.RAM_buffered, - 'free': self.RAM_free, - 'cached': self.RAM_cached, + 'total': self.RAM_total(snmpdump=snmpdump), + 'buffered': self.RAM_buffered(snmpdump=snmpdump), + 'free': self.RAM_free(snmpdump=snmpdump), + 'cached': self.RAM_cached(snmpdump=snmpdump), + }, + 'swap': { + 'total': self.SWAP_total(snmpdump=snmpdump), + 'free': self.SWAP_free(snmpdump=snmpdump), }, - 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free}, } ) return result - def to_dict(self): - return self._dict( + def to_dict(self, snmpdump=None, autowalk=True): + if autowalk: + snmpdump = Trie(self.walk('1.3.6')) + result = self._dict( { 'type': 'DeviceMonitoring', - 'general': {'uptime': self.uptime, 'local_time': self.local_time}, - 'resources': self.resources_to_dict, - 'interfaces': self.interfaces_to_dict, + 'general': { + 'uptime': self.uptime(snmpdump=snmpdump), + 'local_time': self.local_time(snmpdump=snmpdump), + }, + 'resources': self.resources_to_dict(snmpdump=snmpdump), + 'interfaces': self.interfaces_to_dict(snmpdump=snmpdump), } ) + return result diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index 9caff9d..8cbaf69 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -9,6 +9,7 @@ import logging import netaddr +from pysnmp.hlapi import ContextData, ObjectIdentity, ObjectType, SnmpEngine, nextCmd from netengine.backends import BaseBackend from netengine.exceptions import NetEngineError @@ -94,7 +95,26 @@ def _oid(self, oid): # ensure is string (could be unicode) return str(oid) - def get(self, oid): + def walk(self, oid): + result = dict() + for (errorIndication, errorStatus, errorIndex, varBinds) in nextCmd( + SnmpEngine(), + self.community, + self.transport, + ContextData(), + ObjectType(ObjectIdentity(oid)), + lexicographicMode=True, + ): + if errorIndication: + raise NetEngineError(errorIndication) + elif errorStatus: + raise NetEngineError(errorStatus) + else: + for varBind in varBinds: + result[str(varBind[0])] = [None, None, None, [varBind]] + return result + + def get(self, oid, snmpdump=None): """ alias to cmdgen.CommandGenerator().getCmd :oid string|tuple|list: string, tuple or list representing the OID to get @@ -106,10 +126,13 @@ def get(self, oid): * [1, 3, 6, 1, 2, 1, 1, 5, 0] * (1, 3, 6, 1, 2, 1, 1, 5, 0) """ + if snmpdump is not None: + # if OID doesn't exist, then pysnmp returns an empty string + return snmpdump.get(oid, [None, None, None, [[None, '']]]) logger.info(f'DEBUG: SNMP GET {self._oid(oid)}') return self._command.getCmd(self.community, self.transport, self._oid(oid)) - def next(self, oid): + def next(self, oid, snmpdump=None): """ alias to cmdgen.CommandGenerator().nextCmd :oid string|tuple|list: string, tuple or list representing the OID to get @@ -121,21 +144,26 @@ def next(self, oid): * [1, 3, 6, 1, 2, 1, 1, 5, 0] * (1, 3, 6, 1, 2, 1, 1, 5, 0) """ + if snmpdump is not None: + res = [None, 0, 0, []] + for item in snmpdump.items(prefix=oid): + res[3] += [item[1][3]] + return res logger.info(f'DEBUG: SNMP NEXT {self._oid(oid)}') return self._command.nextCmd(self.community, self.transport, self._oid(oid)) - def get_value(self, oid): + def get_value(self, oid, snmpdump=None): """ returns value of oid, or raises NetEngineError Exception is anything wrong :oid string|tuple|list: string, tuple or list representing the OID to get """ - result = self.get(oid) + result = self.get(oid, snmpdump=snmpdump) try: return str(result[3][0][1]) # snmp stores results in several arrays except IndexError: raise NetEngineError(str(result[0])) - def _value_to_retrieve(self): + def _value_to_retrieve(self, snmpdump=None): """ return the final SNMP indexes for the interfaces to be used in the other methods and properties """ @@ -146,7 +174,7 @@ def _value_to_retrieve(self): 'Please fix properly the _oid_to_retrieve string in OpenWRT or AirOS SNMP backend' ) - indexes = self.next(self._oid_to_retrieve)[3] + indexes = self.next(self._oid_to_retrieve, snmpdump=snmpdump)[3] for i in range(len(indexes)): value_to_retr.append(int(indexes[i][0][1])) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 20808f1..c7586fb 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -12,6 +12,7 @@ import pytz from netaddr import EUI, mac_unix_expanded +from pytrie import StringTrie as Trie from netengine.backends.snmp import SNMP @@ -25,62 +26,61 @@ class OpenWRT(SNMP): OpenWRT SNMP backend """ - _oid_to_retrieve = '1.3.6.1.2.1.2.2.1.1' + _oid_to_retrieve = '1.3.6.1.2.1.2.2.1.1.' _interface_dict = {} def __str__(self): """print a human readable object description""" return f'' - def validate(self): + def validate(self, snmpdump=None): """ raises NetEngineError exception if anything is wrong with the connection for example: wrong host, invalid community """ # this triggers a connection which # will raise an exception if anything is wrong - return self.name + return self.name(snmpdump=snmpdump) - @property - def os(self): + def os(self, snmpdump=None): """ returns (os_name, os_version) """ os_name = 'OpenWRT' - os_version = self.get_value('1.3.6.1.2.1.1.1.0').split('#')[0].strip() + os_version = ( + self.get_value('1.3.6.1.2.1.1.1.0', snmpdump=snmpdump).split('#')[0].strip() + ) return os_name, os_version - @property - def manufacturer(self): + def manufacturer(self, snmpdump=None): # TODO: this is dangerous, it might not work in all cases - return self.get_manufacturer(self.interfaces_MAC[1]['mac_address']) + return self.get_manufacturer( + self.interfaces_MAC(snmpdump=snmpdump)[1]['mac_address'] + ) - @property - def name(self): + def name(self, snmpdump=None): """ returns a string containing the device name """ - return self.get_value('1.3.6.1.2.1.1.5.0') + return self.get_value('1.3.6.1.2.1.1.5.0', snmpdump=snmpdump) - @property - def uptime(self): + def uptime(self, snmpdump=None): """ returns an integer representing the number of seconds of uptime """ - return int(self.get_value('1.3.6.1.2.1.1.3.0')) // 100 + return int(self.get_value('1.3.6.1.2.1.1.3.0', snmpdump=snmpdump)) // 100 - @property - def uptime_tuple(self): + def uptime_tuple(self, snmpdump=None): """ returns (days, hours, minutes) """ - td = timedelta(seconds=self.uptime) + td = timedelta(seconds=self.uptime(snmpdump=snmpdump)) return td.days, td.seconds // 3600, (td.seconds // 60) % 60 _interfaces = None - def get_interfaces(self): + def get_interfaces(self, snmpdump=None): """ returns the list of all the interfaces of the device """ @@ -88,11 +88,11 @@ def get_interfaces(self): interfaces = [] value_to_get = '1.3.6.1.2.1.2.2.1.2.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): value_to_get1 = value_to_get + str(i) if value_to_get1: - interfaces.append(self.get_value(value_to_get1)) + interfaces.append(self.get_value(value_to_get1, snmpdump=snmpdump)) self._interfaces = [_f for _f in interfaces if _f] @@ -100,24 +100,28 @@ def get_interfaces(self): _interfaces_MAC = None - @property - def interfaces_MAC(self): + def interfaces_MAC(self, snmpdump=None): """ Returns an ordered dict with the hardware address of every interface """ if self._interfaces_MAC is None: results = [] mac1 = [] - mac = self.next('1.3.6.1.2.1.2.2.1.6.')[3] + mac = self.next('1.3.6.1.2.1.2.2.1.6.', snmpdump=snmpdump)[3] for i in range(1, len(mac) + 1): - mac1.append(self.get_value('1.3.6.1.2.1.2.2.1.6.' + str(i))) + mac1.append( + self.get_value('1.3.6.1.2.1.2.2.1.6.' + str(i), snmpdump=snmpdump) + ) mac_trans = [] for i in mac1: mac_string = self._octet_to_mac(i) mac_trans.append(mac_string) - for i in range(0, len(self.get_interfaces())): + for i in range(0, len(self.get_interfaces(snmpdump=snmpdump))): result = self._dict( - {'name': self.get_interfaces()[i], 'mac_address': mac_trans[i]} + { + 'name': self.get_interfaces(snmpdump=snmpdump)[i], + 'mac_address': mac_trans[i], + } ) results.append(result) @@ -127,8 +131,7 @@ def interfaces_MAC(self): _interfaces_mtu = None - @property - def interfaces_mtu(self): + def interfaces_mtu(self, snmpdump=None): """ Returns an ordereed dict with the interface and its MTU """ @@ -139,11 +142,11 @@ def interfaces_mtu(self): tmp[18] = str(4) to = ''.join(tmp) - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'mtu': int(self.get_value(to + str(i))), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'mtu': int(self.get_value(to + str(i), snmpdump=snmpdump)), } ) results.append(result) @@ -154,8 +157,7 @@ def interfaces_mtu(self): _interfaces_speed = None - @property - def interfaces_speed(self): + def interfaces_speed(self, snmpdump=None): """ Returns an ordered dict with the interface and ist speed in bps """ @@ -176,7 +178,7 @@ def interfaces_speed(self): break # get name - name = self.get_value(starting + str(i)) + name = self.get_value(starting + str(i), snmpdump=snmpdump) # if nothing found if name == '': @@ -191,7 +193,7 @@ def interfaces_speed(self): consecutive_fails = 0 # get speed and convert to int - speed = int(self.get_value(starting_speed + str(i))) + speed = int(self.get_value(starting_speed + str(i), snmpdump=snmpdump)) result = self._dict({'name': name, 'speed': speed}) @@ -205,8 +207,7 @@ def interfaces_speed(self): _interfaces_up = None - @property - def interfaces_up(self): + def interfaces_up(self, snmpdump=None): """ Returns an ordereed dict with the interfaces and their state (up: true/false) """ @@ -216,16 +217,21 @@ def interfaces_up(self): operative = '1.3.6.1.2.1.2.2.1.8.' tmp = list(starting) tmp[18] = str(4) - for i in self._value_to_retrieve(): - if self.get_value(starting + str(i)) != '': + for i in self._value_to_retrieve(snmpdump=snmpdump): + if self.get_value(starting + str(i), snmpdump=snmpdump) != '': result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'up': int(self.get_value(operative + str(i))) == 1, + 'name': self.get_value( + starting + str(i), snmpdump=snmpdump + ), + 'up': int( + self.get_value(operative + str(i), snmpdump=snmpdump) + ) + == 1, } ) results.append(result) - elif self.get_value(starting + str(i)) == '': + elif self.get_value(starting + str(i), snmpdump=snmpdump) == '': result = self._dict({'name': '', 'state': ''}) results.append(result) @@ -235,8 +241,7 @@ def interfaces_up(self): _interfaces_bytes = None - @property - def interfaces_bytes(self): + def interfaces_bytes(self, snmpdump=None): """ Returns an ordereed dict with the interface and its tx and rx octets (1 octet = 1 byte = 8 bits) """ @@ -246,12 +251,16 @@ def interfaces_bytes(self): starting_rx = '1.3.6.1.2.1.2.2.1.10.' starting_tx = '1.3.6.1.2.1.2.2.1.16.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), - 'tx': int(self.get_value(starting_tx + str(i))), - 'rx': int(self.get_value(starting_rx + str(i))), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'tx': int( + self.get_value(starting_tx + str(i), snmpdump=snmpdump) + ), + 'rx': int( + self.get_value(starting_rx + str(i), snmpdump=snmpdump) + ), } ) results.append(result) @@ -262,8 +271,7 @@ def interfaces_bytes(self): _interfaces_type = None - @property - def interfaces_type(self): + def interfaces_type(self, snmpdump=None): """ Returns an ordered dict with the interface type (e.g Ethernet, loopback) """ @@ -271,12 +279,13 @@ def interfaces_type(self): results = [] starting = '1.3.6.1.2.1.2.2.1.2.' types_oid = '1.3.6.1.2.1.2.2.1.3.' - for i in self._value_to_retrieve(): + for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { - 'name': self.get_value(starting + str(i)), + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), 'type': ifTypes.get( - self.get_value(types_oid + str(i)), 'unknown' + self.get_value(types_oid + str(i), snmpdump=snmpdump), + 'unknown', ), } ) @@ -287,20 +296,23 @@ def interfaces_type(self): _interface_addr_and_mask = None - @property - def interface_addr_and_mask(self): + def interface_addr_and_mask(self, snmpdump=None): """ TODO: this method needs to be simplified and explained """ if self._interface_addr_and_mask is None: - interface_name = self.get_interfaces() + interface_name = self.get_interfaces(snmpdump=snmpdump) for i in range(0, len(interface_name)): - self._interface_dict[self._value_to_retrieve()[i]] = interface_name[i] + self._interface_dict[ + self._value_to_retrieve(snmpdump=snmpdump)[i] + ] = interface_name[i] - interface_ip_address = self.next('1.3.6.1.2.1.4.20.1.1')[3] - interface_index = self.next('1.3.6.1.2.1.4.20.1.2')[3] - interface_netmask = self.next('1.3.6.1.2.1.4.20.1.3')[3] + interface_ip_address = self.next( + '1.3.6.1.2.1.4.20.1.1.', snmpdump=snmpdump + )[3] + interface_index = self.next('1.3.6.1.2.1.4.20.1.2.', snmpdump=snmpdump)[3] + interface_netmask = self.next('1.3.6.1.2.1.4.20.1.3.', snmpdump=snmpdump)[3] results = {} @@ -318,30 +330,29 @@ def interface_addr_and_mask(self): return self._interface_addr_and_mask - @property - def interfaces_to_dict(self): + def interfaces_to_dict(self, snmpdump=None): """ Returns an ordered dict with all the information available about the interface """ results = [] - for i in range(0, len(self.get_interfaces())): + for i in range(0, len(self.get_interfaces(snmpdump=snmpdump))): logger.info(f'====== {i} ======') logger.info('... name ...') - name = self.interfaces_MAC[i]['name'] + name = self.interfaces_MAC(snmpdump=snmpdump)[i]['name'] logger.info('... if_type ...') - if_type = self.interfaces_type[i]['type'] + if_type = self.interfaces_type(snmpdump=snmpdump)[i]['type'] logger.info('... mac_address ...') - mac_address = self.interfaces_MAC[i]['mac_address'] + mac_address = self.interfaces_MAC(snmpdump=snmpdump)[i]['mac_address'] logger.info('... rx_bytes ...') - rx_bytes = int(self.interfaces_bytes[i]['rx']) + rx_bytes = int(self.interfaces_bytes(snmpdump=snmpdump)[i]['rx']) logger.info('... tx_bytes ...') - tx_bytes = int(self.interfaces_bytes[i]['tx']) + tx_bytes = int(self.interfaces_bytes(snmpdump=snmpdump)[i]['tx']) logger.info('... up ...') - up = self.interfaces_up[i]['up'] + up = self.interfaces_up(snmpdump=snmpdump)[i]['up'] logger.info('... mtu ...') - mtu = int(self.interfaces_mtu[i]['mtu']) + mtu = int(self.interfaces_mtu(snmpdump=snmpdump)[i]['mtu']) result = self._dict( { @@ -359,12 +370,11 @@ def interfaces_to_dict(self): results.append(result) return results - @property - def local_time(self): + def local_time(self, snmpdump=None): """ 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]) + octetstr = bytes(self.get('1.3.6.1.2.1.25.1.2.0', snmpdump=snmpdump)[3][0][1]) size = len(octetstr) # string may or may not contain timezone, so size can be 8 or 11 if size == 8: @@ -412,86 +422,90 @@ def local_time(self): ).timestamp() ) - @property - def RAM_total(self): + def RAM_total(self, snmpdump=None): """ returns the total RAM of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.1')) + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.1', snmpdump=snmpdump)) - @property - def RAM_shared(self): + def RAM_shared(self, snmpdump=None): """ returns the shared RAM of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.8')) + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.8', snmpdump=snmpdump)) - @property - def RAM_cached(self): + def RAM_cached(self, snmpdump=None): """ returns the cached RAM of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.7')) + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.7', snmpdump=snmpdump)) - @property - def RAM_used(self): + def RAM_used(self, snmpdump=None): """ returns the used RAM of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.1')) + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.1', snmpdump=snmpdump)) - @property - def RAM_free(self): + def RAM_free(self, snmpdump=None): """ returns the free RAM of the device """ - return int(self.RAM_total - (self.RAM_used - self.RAM_cached)) + return int( + self.RAM_total(snmpdump=snmpdump) + - (self.RAM_used(snmpdump=snmpdump) - self.RAM_cached(snmpdump=snmpdump)) + ) - @property - def SWAP_total(self): + def SWAP_total(self, snmpdump=None): """ returns the total SWAP of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.10')) + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.10', snmpdump=snmpdump)) - @property - def SWAP_free(self): + def SWAP_used(self, snmpdump=None): + """ + returns the used SWAP of the device + """ + return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.10', snmpdump=snmpdump)) + + def SWAP_free(self, snmpdump=None): """ 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 + SWAP_free = self.SWAP_total(snmpdump=snmpdump) - self.SWAP_used( + snmpdump=snmpdump + ) return SWAP_free - @property - def CPU_count(self): + def CPU_count(self, snmpdump=None): """ returns the count of CPUs of the device """ - return len(self.next('1.3.6.1.2.1.25.3.3.1.2')[3]) + return len(self.next('1.3.6.1.2.1.25.3.3.1.2.', snmpdump=snmpdump)[3]) - @property - def resources_to_dict(self): + def resources_to_dict(self, snmpdump=None): """ returns an ordered dict with hardware resources information """ result = self._dict( { - 'cpus': self.CPU_count, + 'cpus': self.CPU_count(snmpdump=snmpdump), 'memory': { - 'total': self.RAM_total, - 'shared': self.RAM_shared, - 'used': self.RAM_used, - 'free': self.RAM_free, - 'cached': self.RAM_cached, + 'total': self.RAM_total(snmpdump=snmpdump), + 'shared': self.RAM_shared(snmpdump=snmpdump), + 'used': self.RAM_used(snmpdump=snmpdump), + 'free': self.RAM_free(snmpdump=snmpdump), + 'cached': self.RAM_cached(snmpdump=snmpdump), + }, + 'swap': { + 'total': self.SWAP_total(snmpdump=snmpdump), + 'free': self.SWAP_free(snmpdump=snmpdump), + 'used': self.SWAP_used(snmpdump=snmpdump), }, - 'swap': {'total': self.SWAP_total, 'free': self.SWAP_free}, } ) return result - @property - def neighbors(self): + def neighbors(self, snmpdump=None): """ returns a dict with neighbors information """ @@ -507,7 +521,7 @@ def neighbors(self): neighbors_oid = '1.3.6.1.2.1.4.35.1.4' neighbor_states_oid = '1.3.6.1.2.1.4.35.1.7' - neighbor_info = self.next('1.3.6.1.2.1.4.35.1')[3] + neighbor_info = self.next('1.3.6.1.2.1.4.35.1.', snmpdump=snmpdump)[3] neighbors = [] neighbor_states = [] result = [] @@ -529,7 +543,9 @@ def neighbors(self): int(neighbor[0][1].prettyPrint(), 16), dialect=mac_unix_expanded ) interface_num = neighbor[0][0].getOid()[10] - interface = self.get(f'1.3.6.1.2.1.31.1.1.1.1.{interface_num}')[3][0][1] + interface = self.get( + f'1.3.6.1.2.1.31.1.1.1.1.{interface_num}', snmpdump=snmpdump + )[3][0][1] state = states_map[str(neighbor_states[index][0][1])] except (IndexError, TypeError, ValueError): continue @@ -545,13 +561,19 @@ def neighbors(self): ) return result - def to_dict(self): - return self._dict( + def to_dict(self, snmpdump=None, autowalk=True): + if autowalk: + snmpdump = Trie(self.walk('1.3.6')) + result = self._dict( { 'type': 'DeviceMonitoring', - 'general': {'uptime': self.uptime, "local_time": self.local_time}, - 'resources': self.resources_to_dict, - 'interfaces': self.interfaces_to_dict, - 'neighbors': self.neighbors, + 'general': { + 'uptime': self.uptime(snmpdump=snmpdump), + "local_time": self.local_time(snmpdump=snmpdump), + }, + 'resources': self.resources_to_dict(snmpdump=snmpdump), + 'interfaces': self.interfaces_to_dict(snmpdump=snmpdump), + 'neighbors': self.neighbors(snmpdump=snmpdump), } ) + return result diff --git a/requirements.txt b/requirements.txt index 81d4504..b709a55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ netaddr~=0.8.0 pysnmp~=4.4.12 +pytrie~=0.4.0 diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 7c5120c..54db4d6 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -24,6 +24,6 @@ def test_get_manufacturer(self): self.assertIn('Xensource, Inc.', str(self.dummy.get_manufacturer(dummy_addr))) def test_to_json(self): - json_string = self.dummy.to_json() + json_string = self.dummy.to_json(autowalk=False) self.assertTrue(isinstance(json_string, str)) json.loads(json_string) diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index 08e7b58..bfbe3e9 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -50,7 +50,8 @@ def test_get_value_error(self): def test_validate_negative_result(self): self.getcmd_patcher.stop() wrong = AirOS('10.40.0.254', 'wrong', 'wrong') - self.assertRaises(NetEngineError, wrong.validate) + with self.assertRaises(NetEngineError): + wrong.validate() def test_validate_positive_result(self): self.device.validate() @@ -68,95 +69,95 @@ def test_get(self): def test_properties(self): device = self.device - device.os - device.name - device.model - device.os - device.uptime + device.os() + device.name() + device.model() + device.os() + device.uptime() def test_name(self): - self.assertIsInstance(self.device.name, str) + self.assertIsInstance(self.device.name(), str) def test_os(self): - self.assertIsInstance(self.device.os, tuple) + self.assertIsInstance(self.device.os(), tuple) def test_get_interfaces(self): self.assertIsInstance(self.device.get_interfaces(), list) def test_get_interfaces_mtu(self): - self.assertIsInstance(self.device.interfaces_mtu, list) + self.assertIsInstance(self.device.interfaces_mtu(), list) def test_interfaces_state(self): - self.assertIsInstance(self.device.interfaces_state, list) + self.assertIsInstance(self.device.interfaces_state(), list) def test_interfaces_speed(self): - self.assertIsInstance(self.device.interfaces_speed, list) + self.assertIsInstance(self.device.interfaces_speed(), list) def test_interfaces_bytes(self): - self.assertIsInstance(self.device.interfaces_bytes, list) + self.assertIsInstance(self.device.interfaces_bytes(), list) def test_interfaces_MAC(self): - self.assertIsInstance(self.device.interfaces_MAC, list) + self.assertIsInstance(self.device.interfaces_MAC(), list) def test_interfaces_type(self): - self.assertIsInstance(self.device.interfaces_type, list) + self.assertIsInstance(self.device.interfaces_type(), list) def test_interfaces_to_dict(self): - self.assertIsInstance(self.device.interfaces_to_dict, list) + self.assertIsInstance(self.device.interfaces_to_dict(), list) def test_wireless_dbm(self): - self.assertIsInstance(self.device.wireless_dbm, list) + self.assertIsInstance(self.device.wireless_dbm(), list) def test_interfaces_number(self): - self.assertIsInstance(self.device.interfaces_number, int) + self.assertIsInstance(self.device.interfaces_number(), int) def test_wireless_to_dict(self): - self.assertIsInstance(self.device.wireless_links, list) + self.assertIsInstance(self.device.wireless_links(), list) def test_RAM_free(self): - self.assertIsInstance(self.device.RAM_free, int) + self.assertIsInstance(self.device.RAM_free(), int) def test_RAM_total(self): - self.assertIsInstance(self.device.RAM_total, int) + self.assertIsInstance(self.device.RAM_total(), int) def test_to_dict(self): - self.assertTrue(isinstance(self.device.to_dict(), dict)) + self.assertTrue(isinstance(self.device.to_dict(autowalk=False), dict)) def test_netjson_compliance(self): - device_dict = self.device.to_dict() - device_json = self.device.to_json() + device_dict = self.device.to_dict(autowalk=False) + device_json = self.device.to_json(autowalk=False) validate(instance=device_dict, schema=schema) validate(instance=json.loads(device_json), schema=schema) def test_manufacturer(self): - self.assertIsNotNone(self.device.manufacturer) + self.assertIsNotNone(self.device.manufacturer()) def test_model(self): - self.assertIsInstance(self.device.model, str) + self.assertIsInstance(self.device.model(), str) def test_firmware(self): - self.assertIsInstance(self.device.firmware, str) + self.assertIsInstance(self.device.firmware(), str) def test_uptime(self): - self.assertIsInstance(self.device.uptime, int) + self.assertIsInstance(self.device.uptime(), int) def test_RAM_buffered(self): - self.assertIsInstance(self.device.RAM_buffered, int) + self.assertIsInstance(self.device.RAM_buffered(), int) def test_RAM_cached(self): - self.assertIsInstance(self.device.RAM_cached, int) + self.assertIsInstance(self.device.RAM_cached(), int) def test_SWAP_total(self): - self.assertIsInstance(self.device.SWAP_total, int) + self.assertIsInstance(self.device.SWAP_total(), int) def test_SWAP_free(self): - self.assertIsInstance(self.device.SWAP_free, int) + self.assertIsInstance(self.device.SWAP_free(), int) def test_local_time(self): - self.assertIsInstance(self.device.local_time, int) + self.assertIsInstance(self.device.local_time(), int) def test_load(self): - load = self.device.load + load = self.device.load() self.assertIsInstance(load, list) self.assertEqual(len(load), 3) self.assertIsInstance(load[0], int) diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index e81e2f8..c48c26e 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -41,87 +41,87 @@ def setUp(self): self.nextcmd_patcher.start() def test_os(self): - self.assertIsInstance(self.device.os, tuple) + self.assertIsInstance(self.device.os(), tuple) def test_manufacturer(self): - self.assertIsNotNone(self.device.manufacturer) + self.assertIsNotNone(self.device.manufacturer()) def test_name(self): - self.assertIsInstance(self.device.name, str) + self.assertIsInstance(self.device.name(), str) def test_uptime(self): - self.assertIsInstance(self.device.uptime, int) + self.assertIsInstance(self.device.uptime(), int) def test_uptime_tuple(self): - self.assertIsInstance(self.device.uptime_tuple, tuple) + self.assertIsInstance(self.device.uptime_tuple(), tuple) def test_get_interfaces(self): self.assertIsInstance(self.device.get_interfaces(), list) def test_interfaces_speed(self): - self.assertIsInstance(self.device.interfaces_speed, list) + self.assertIsInstance(self.device.interfaces_speed(), list) def test_interfaces_bytes(self): - self.assertIsInstance(self.device.interfaces_bytes, list) + self.assertIsInstance(self.device.interfaces_bytes(), list) def test_interfaces_MAC(self): - self.assertIsInstance(self.device.interfaces_MAC, list) + self.assertIsInstance(self.device.interfaces_MAC(), list) def test_interfaces_type(self): - self.assertIsInstance(self.device.interfaces_type, list) + self.assertIsInstance(self.device.interfaces_type(), list) def test_interfaces_mtu(self): - self.assertIsInstance(self.device.interfaces_mtu, list) + self.assertIsInstance(self.device.interfaces_mtu(), list) def test_interfaces_state(self): - self.assertIsInstance(self.device.interfaces_up, list) + self.assertIsInstance(self.device.interfaces_up(), list) def test_interfaces_to_dict(self): - self.assertIsInstance(self.device.interfaces_to_dict, list) + self.assertIsInstance(self.device.interfaces_to_dict(), list) def test_interface_addr_and_mask(self): - self.assertIsInstance(self.device.interface_addr_and_mask, dict) + self.assertIsInstance(self.device.interface_addr_and_mask(), dict) def test_RAM_total(self): - self.assertIsInstance(self.device.RAM_total, int) + self.assertIsInstance(self.device.RAM_total(), int) def test_RAM_shared(self): - self.assertIsInstance(self.device.RAM_shared, int) + self.assertIsInstance(self.device.RAM_shared(), int) def test_RAM_cached(self): - self.assertIsInstance(self.device.RAM_cached, int) + self.assertIsInstance(self.device.RAM_cached(), int) def test_RAM_used(self): - self.assertIsInstance(self.device.RAM_used, int) + self.assertIsInstance(self.device.RAM_used(), int) def test_RAM_free(self): - self.assertIsInstance(self.device.RAM_free, int) + self.assertIsInstance(self.device.RAM_free(), int) def test_SWAP_total(self): - self.assertIsInstance(self.device.SWAP_total, int) + self.assertIsInstance(self.device.SWAP_total(), int) def test_SWAP_free(self): - self.assertIsInstance(self.device.SWAP_free, int) + self.assertIsInstance(self.device.SWAP_free(), int) def test_CPU_count(self): - self.assertIsInstance(self.device.CPU_count, int) + self.assertIsInstance(self.device.CPU_count(), int) def test_neighbors(self): - self.assertIsInstance(self.device.neighbors, list) + self.assertIsInstance(self.device.neighbors(), list) def test_local_time(self): - self.assertIsInstance(self.device.local_time, int) + self.assertIsInstance(self.device.local_time(), int) def test_to_dict(self): - device_dict = self.device.to_dict() + device_dict = self.device.to_dict(autowalk=False) self.assertIsInstance(device_dict, dict) self.assertEqual( len(device_dict['interfaces']), len(self.device.get_interfaces()), ) def test_netjson_compliance(self): - device_dict = self.device.to_dict() - device_json = self.device.to_json() + device_dict = self.device.to_dict(autowalk=False) + device_json = self.device.to_json(autowalk=False) validate(instance=device_dict, schema=schema) validate(instance=json.loads(device_json), schema=schema) diff --git a/tests/utils.py b/tests/utils.py index 0e1e2bb..e0ce095 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -59,18 +59,18 @@ def _get_nextcmd_list(return_value): return [None, None, None, return_value] res = { - '1.3.6.1.4.1.14988.1.1.1.2.1': [[[0, 0], 0]] * 28, - '1.3.6.1.4.1.14988.1.1.1.2.1.3': [0, 0], - '1.3.6.1.4.1.14988.1.1.1.2.1.3.0': [], - '1.3.6.1.2.1.1.9.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + '1.3.6.1.4.1.14988.1.1.1.2.1.': [[[0, 0], 0]] * 28, + '1.3.6.1.4.1.14988.1.1.1.2.1.3.': [0, 0], + '1.3.6.1.4.1.14988.1.1.1.2.1.3.0.': [], + '1.3.6.1.2.1.1.9.1.1.': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], '1.3.6.1.2.1.2.2.1.6.': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], - '1.3.6.1.2.1.2.2.1.1': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], - '1.3.6.1.2.1.4.20.1.1': [[[0, OctetString('127.0.0.1')]]], - '1.3.6.1.2.1.4.20.1.2': [[[0, 1]]], - '1.3.6.1.2.1.25.3.3.1.2': [0, 2], - '1.3.6.1.2.1.4.20.1.3': [[[0, OctetString('192.168.0.1')]]], - '1.3.6.1.4.1.10002.1.1.1.4.2.1.3': [[[0, 51]], [[0, 18]], [[0, 24]]], - '1.3.6.1.2.1.4.35.1': [ + '1.3.6.1.2.1.2.2.1.1.': [[[0, 1]], [[0, 2]], [[0, 3]], [[0, 4]], [[0, 5]]], + '1.3.6.1.2.1.4.20.1.1.': [[[0, OctetString('127.0.0.1')]]], + '1.3.6.1.2.1.4.20.1.2.': [[[0, 1]]], + '1.3.6.1.2.1.25.3.3.1.2.': [0, 2], + '1.3.6.1.2.1.4.20.1.3.': [[[0, OctetString('192.168.0.1')]]], + '1.3.6.1.4.1.10002.1.1.1.4.2.1.3.': [[[0, 51]], [[0, 18]], [[0, 24]]], + '1.3.6.1.2.1.4.35.1.': [ [[[MockOid('1.3.6.1.2.1.4.35.1.4')]], OctetString('0x040e3cca555f')], [[[MockOid('1.3.6.1.2.1.4.35.1.7')]], 1], ], From 83a25dee3fb9196a87e3198555f7e48606c05633 Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 22 Jul 2021 18:37:08 +0530 Subject: [PATCH 14/20] [change] Add device name, ip to interfaces --- netengine/backends/snmp/openwrt.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index c7586fb..5c8fc21 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -62,7 +62,7 @@ def name(self, snmpdump=None): """ returns a string containing the device name """ - return self.get_value('1.3.6.1.2.1.1.5.0', snmpdump=snmpdump) + return self.get_value('1.3.6.1.2.1.1.1.0', snmpdump=snmpdump).split()[1] def uptime(self, snmpdump=None): """ @@ -353,6 +353,12 @@ def interfaces_to_dict(self, snmpdump=None): up = self.interfaces_up(snmpdump=snmpdump)[i]['up'] logger.info('... mtu ...') mtu = int(self.interfaces_mtu(snmpdump=snmpdump)[i]['mtu']) + logger.info('... if_ip ...') + if_ip = ( + self.interface_addr_and_mask(snmpdump=snmpdump) + .get(name, {}) + .get('address', '') + ) result = self._dict( { @@ -364,6 +370,7 @@ def interfaces_to_dict(self, snmpdump=None): 'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes, "mtu": mtu, + "ip": if_ip, }, } ) @@ -568,8 +575,9 @@ def to_dict(self, snmpdump=None, autowalk=True): { 'type': 'DeviceMonitoring', 'general': { + 'name': self.name(snmpdump=snmpdump), 'uptime': self.uptime(snmpdump=snmpdump), - "local_time": self.local_time(snmpdump=snmpdump), + 'local_time': self.local_time(snmpdump=snmpdump), }, 'resources': self.resources_to_dict(snmpdump=snmpdump), 'interfaces': self.interfaces_to_dict(snmpdump=snmpdump), From 0f377173bb4ac375f3d94a6c7c69eac3372be791 Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 22 Jul 2021 22:13:28 +0530 Subject: [PATCH 15/20] [change] Make interface types NetJSON compliant --- netengine/backends/snmp/airos.py | 14 +- netengine/backends/snmp/openwrt.py | 10 +- netengine/backends/snmp/utils.py | 236 ----------------------------- 3 files changed, 17 insertions(+), 243 deletions(-) delete mode 100644 netengine/backends/snmp/utils.py diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 5145439..22edd22 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -284,7 +284,12 @@ def interfaces_type(self, snmpdump=None): Returns an ordered dict with the interface type (e.g Ethernet, loopback) """ if self._interfaces_type is None: - types = {'6': 'ethernetCsmacd', '24': 'softwareLoopback'} + types = { + '6': 'ethernet', + '24': 'loopback', + '157': 'wireless', + '209': 'bridge', + } results = [] starting = '1.3.6.1.2.1.2.2.1.2.' types_oid = '1.3.6.1.2.1.2.2.1.3.' @@ -293,9 +298,10 @@ def interfaces_type(self, snmpdump=None): result = self._dict( { 'name': self.get_value(starting + str(i), snmpdump=snmpdump), - 'type': types[ - self.get_value(types_oid + str(i), snmpdump=snmpdump) - ], + 'type': types.get( + self.get_value(types_oid + str(i), snmpdump=snmpdump), + 'unknown', + ), } ) results.append(result) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 5c8fc21..94cf534 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -16,8 +16,6 @@ from netengine.backends.snmp import SNMP -from .utils import ifTypes - logger = logging.getLogger(__name__) @@ -279,11 +277,17 @@ def interfaces_type(self, snmpdump=None): results = [] starting = '1.3.6.1.2.1.2.2.1.2.' types_oid = '1.3.6.1.2.1.2.2.1.3.' + types = { + '6': 'ethernet', + '24': 'loopback', + '157': 'wireless', + '209': 'bridge', + } for i in self._value_to_retrieve(snmpdump=snmpdump): result = self._dict( { 'name': self.get_value(starting + str(i), snmpdump=snmpdump), - 'type': ifTypes.get( + 'type': types.get( self.get_value(types_oid + str(i), snmpdump=snmpdump), 'unknown', ), diff --git a/netengine/backends/snmp/utils.py b/netengine/backends/snmp/utils.py deleted file mode 100644 index aa72815..0000000 --- a/netengine/backends/snmp/utils.py +++ /dev/null @@ -1,236 +0,0 @@ -ifTypes = { - '1': 'other', - '2': 'regular1822', - '3': 'hdh1822', - '4': 'ddnX25', - '5': 'rfc877x25', - '6': 'ethernet Csmacd', - '7': 'iso88023 Csmacd', - '8': 'iso88024TokenBus', - '9': 'iso88025TokenRing', - '10': 'iso88026Man', - '11': 'starLan', - '12': 'proteon10Mbit', - '13': 'proteon80Mbit', - '14': 'hyperchannel', - '15': 'fddi', - '16': 'lapb', - '17': 'sdlc', - '18': 'ds1', - '19': 'e1', - '20': 'basic ISDN', - '21': 'primary ISDN', - '22': 'prop PointToPoint Serial', - '23': 'ppp', - '24': 'software loopback', - '25': 'eon', - '26': 'ethernet 3Mbit', - '27': 'nsip', - '28': 'slip', - '29': 'ultra', - '30': 'ds3', - '31': 'sip', - '32': 'frameRelay', - '33': 'rs232', - '34': 'para', - '35': 'arcnet', - '36': 'arcnetPlus', - '37': 'atm', - '38': 'miox25', - '39': 'sonet', - '40': 'x25ple', - '41': 'iso88022llc', - '42': 'localTalk', - '43': 'smdsDxi', - '44': 'frameRelayService', - '45': 'v35', - '46': 'hssi', - '47': 'hippi', - '48': 'modem', - '49': 'aal5', - '50': 'sonetPath', - '51': 'sonetVT', - '52': 'smdsIcip', - '53': 'propVirtual', - '54': 'propMultiplexor', - '55': 'ieee80212', - '56': 'fibreChannel', - '57': 'hippiInterface', - '58': 'frameRelayInterconnect', - '59': 'aflane8023', - '60': 'aflane8025', - '61': 'cctEmul', - '62': 'fastEther', - '63': 'isdn', - '64': 'v11', - '65': 'v36', - '66': 'g703at64k', - '67': 'g703at2mb', - '68': 'qllc', - '69': 'fastEtherFX', - '70': 'channel', - '71': 'ieee 80211', - '72': 'ibm370parChan', - '73': 'escon', - '74': 'dlsw', - '75': 'isdns', - '76': 'isdnu', - '77': 'lapd', - '78': 'ipSwitch', - '79': 'rsrb', - '80': 'atmLogical', - '81': 'ds0', - '82': 'ds0Bundle', - '83': 'bsc', - '84': 'async', - '85': 'cnr', - '86': 'iso88025Dtr', - '87': 'eplrs', - '88': 'arap', - '89': 'propCnls', - '90': 'hostPad', - '91': 'termPad', - '92': 'frameRelayMPI', - '93': 'x213', - '94': 'adsl', - '95': 'radsl', - '96': 'sdsl', - '97': 'vdsl', - '98': 'iso88025CRFPInt', - '99': 'myrinet', - '100': 'voiceEM', - '101': 'voiceFXO', - '102': 'voiceFXS', - '103': 'voiceEncap', - '104': 'voiceOverIp', - '105': 'atmDxi', - '106': 'atmFuni', - '107': 'atmIma', - '108': 'pppMultilinkBundle', - '109': 'ipOverCdlc', - '110': 'ipOverClaw', - '111': 'stackToStack', - '112': 'virtualIpAddress', - '113': 'mpc', - '114': 'ipOverAtm', - '115': 'iso88025Fiber', - '116': 'tdlc', - '117': 'gigabitEthernet', - '118': 'hdlc', - '119': 'lapf', - '120': 'v37', - '121': 'x25mlp', - '122': 'x25huntGroup', - '123': 'trasnpHdlc', - '124': 'interleave', - '125': 'fast', - '126': 'ip', - '127': 'docsCableMaclayer', - '128': 'docsCableDownstream', - '129': 'docsCableUpstream', - '130': 'a12MppSwitch', - '131': 'tunnel', - '132': 'coffee', - '133': 'ces', - '134': 'atmSubInterface', - '135': 'l2vlan', - '136': 'l3ipvlan', - '137': 'l3ipxvlan', - '138': 'digitalPowerline', - '139': 'mediaMailOverIp', - '140': 'dtm', - '141': 'dcn', - '142': 'ipForward', - '143': 'msdsl', - '144': 'ieee1394', - '145': 'if-gsn', - '146': 'dvbRccMacLayer', - '147': 'dvbRccDownstream', - '148': 'dvbRccUpstream', - '149': 'atmVirtual', - '150': 'mplsTunnel', - '151': 'srp', - '152': 'voiceOverAtm', - '153': 'voiceOverFrameRelay', - '154': 'idsl', - '155': 'compositeLink', - '156': 'ss7SigLink', - '157': 'propWirelessP2P', - '158': 'frForward', - '159': 'rfc1483', - '160': 'usb', - '161': '802.3ad link aggregation', - '162': 'bgp policy accounting', - '163': 'frf16MfrBundle', - '164': 'h323Gatekeeper', - '165': 'h323Proxy', - '166': 'mpls', - '167': 'mfSigLink', - '168': 'hdsl2', - '169': 'shdsl', - '170': 'ds1FDL', - '171': 'pos', - '172': 'dvbAsiIn', - '173': 'dvbAsiOut', - '174': 'plc', - '175': 'nfas', - '176': 'tr008', - '177': 'gr303RDT', - '178': 'gr303IDT', - '179': 'isup', - '180': 'propDocsWirelessMaclayer', - '181': 'propDocsWirelessDownstream', - '182': 'propDocsWirelessUpstream', - '183': 'hiperlan2', - '184': 'propBWAp2Mp', - '185': 'sonetOverheadChannel', - '186': 'digitalWrapperOverheadChannel', - '187': 'aal2', - '188': 'radioMAC', - '189': 'atmRadio', - '190': 'imt', - '191': 'mvl', - '192': 'reachDSL', - '193': 'frDlciEndPt', - '194': 'atmVciEndPt', - '195': 'opticalChannel', - '196': 'opticalTransport', - '197': 'propAtm', - '198': 'voiceOverCable', - '199': 'infiniband', - '200': 'teLink', - '201': 'q2931', - '202': 'virtualTg', - '203': 'sipTg', - '204': 'sipSig', - '205': 'docsCableUpstreamChannel', - '206': 'econet', - '207': 'pon155', - '208': 'pon622', - '209': 'bridge', - '210': 'linegroup', - '211': 'voiceEMFGD', - '212': 'voiceFGDEANA', - '213': 'voiceDID', - '214': 'mpegTransport', - '215': 'sixToFour', - '216': 'gtp', - '217': 'pdnEtherLoop1', - '218': 'pdnEtherLoop2', - '219': 'opticalChannelGroup', - '220': 'homepna', - '221': 'gfp', - '222': 'ciscoISLvlan', - '223': 'actelisMetaLOOP', - '224': 'fcipLink', - '225': 'rpr', - '226': 'qam', - '227': 'lmp', - '228': 'cblVectaStar', - '229': 'docsCableMCmtsDownstream', - '230': 'adsl2', - '231': 'macSecControlledIF', - '232': 'macSecUncontrolledIF', - '233': 'aviciOpticalEther', - '234': 'atmbond', -} From 5dc3cc66a5a6f873886953953e5e00d0d4c07d00 Mon Sep 17 00:00:00 2001 From: purhan Date: Mon, 26 Jul 2021 21:53:51 +0530 Subject: [PATCH 16/20] [change] Change RAM OIDs, add load info, add wireless interfaces --- netengine/backends/snmp/airos.py | 86 +++++++++++++----- netengine/backends/snmp/openwrt.py | 112 +++++++++++++++--------- tests/static/test-openwrt-snmp-oid.json | 17 +++- tests/test_snmp/test_airos.py | 6 +- tests/test_snmp/test_openwrt.py | 11 ++- tests/utils.py | 1 + 6 files changed, 162 insertions(+), 71 deletions(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 22edd22..06418b0 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -11,6 +11,8 @@ from pytrie import StringTrie as Trie +from netengine.exceptions import NetEngineError + from .base import SNMP logger = logging.getLogger(__name__) @@ -278,6 +280,7 @@ def interfaces_MAC(self, snmpdump=None): return self._interfaces_MAC _interfaces_type = None + _wireless_interfaces = None def interfaces_type(self, snmpdump=None): """ @@ -310,24 +313,59 @@ def interfaces_type(self, snmpdump=None): return self._interfaces_type + def get_wireless_interfaces(self, snmpdump=None): + """ + returns the list of all the wireless interfaces of the device + """ + if self._wireless_interfaces is None: + interfaces = [] + wireless_if_oid = '1.2.840.10036.1.1.1.1.' + interfaces_oid = '1.3.6.1.2.1.2.2.1.2.' + + for i in self._value_to_retrieve(snmpdump=snmpdump): + try: + value_to_get1 = self.get_value( + wireless_if_oid + str(i), snmpdump=snmpdump + ) + + if value_to_get1: + interfaces.append( + self.get_value(interfaces_oid + str(i), snmpdump=snmpdump) + ) + except (NetEngineError, KeyError): + pass + + self._wireless_interfaces = [_f for _f in interfaces if _f] + + return self._wireless_interfaces + + _interfaces_MAC = None + def interfaces_to_dict(self, snmpdump=None): """ Returns an ordered dict with all the information available about the interface """ results = [] + wireless_if = self.get_wireless_interfaces() for i in range(0, len(self.get_interfaces(snmpdump=snmpdump))): logger.info(f'===== {i} =====') + logger.info('... name ...') + name = self.interfaces_MAC(snmpdump=snmpdump)[i]['name'] + logger.info('... if_type ...') + if_type = self.interfaces_type(snmpdump=snmpdump)[i]['type'] + logger.info('... rx_bytes ...') + rx_bytes = int(self.interfaces_bytes(snmpdump=snmpdump)[i]['rx']) + logger.info('... tx_bytes ...') + tx_bytes = int(self.interfaces_bytes(snmpdump=snmpdump)[i]['tx']) + + if name in wireless_if: + if_type = 'wireless' + result = self._dict( { - 'name': self.interfaces_MAC(snmpdump=snmpdump)[i]['name'], - 'statistics': { - 'rx_bytes': int( - self.interfaces_bytes(snmpdump=snmpdump)[i]['rx'] - ), - 'tx_bytes': int( - self.interfaces_bytes(snmpdump=snmpdump)[i]['tx'] - ), - }, + 'name': name, + "type": if_type, + 'statistics': {'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes}, } ) results.append(result) @@ -398,31 +436,31 @@ def local_time(self, snmpdump=None): def RAM_total(self, snmpdump=None): """ - Returns the total RAM of the device + Returns the total RAM of the device in bytes """ total = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.1.0', snmpdump=snmpdump) - return int(total) + return int(total) * 1024 def RAM_free(self, snmpdump=None): """ - Returns the free RAM of the device + Returns the free RAM of the device in bytes """ free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0', snmpdump=snmpdump) - return int(free) + return int(free) * 1024 def RAM_buffered(self, snmpdump=None): """ - Returns the buffered RAM of the device + Returns the buffered RAM of the device in bytes """ buffered = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.3.0', snmpdump=snmpdump) - return int(buffered) + return int(buffered) * 1024 def RAM_cached(self, snmpdump=None): """ - Returns the cached RAM of the device + Returns the cached RAM of the device in bytes """ cached = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.4.0', snmpdump=snmpdump) - return int(cached) + return int(cached) * 1024 def load(self, snmpdump=None): """ @@ -430,24 +468,24 @@ def load(self, snmpdump=None): minute, in the last 5 minutes and in the last 15 minutes """ array = self.next('1.3.6.1.4.1.10002.1.1.1.4.2.1.3.', snmpdump=snmpdump)[3] - one = int(array[0][0][1]) - five = int(array[1][0][1]) - fifteen = int(array[2][0][1]) + one = float(array[0][0][1]) / 100 + five = float(array[1][0][1]) / 100 + fifteen = float(array[2][0][1]) / 100 return [one, five, fifteen] def SWAP_total(self, snmpdump=None): """ - Returns the total SWAP of the device + Returns the total SWAP of the device in bytes """ total = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.1.0', snmpdump=snmpdump) - return int(total) + return int(total * 1024) def SWAP_free(self, snmpdump=None): """ - Returns the free SWAP of the device + Returns the free SWAP of the device in bytes """ free = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.2.0', snmpdump=snmpdump) - return int(free) + return int(free * 1024) def resources_to_dict(self, snmpdump=None): """ diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 94cf534..ea5a52c 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -15,6 +15,7 @@ from pytrie import StringTrie as Trie from netengine.backends.snmp import SNMP +from netengine.exceptions import NetEngineError logger = logging.getLogger(__name__) @@ -77,6 +78,7 @@ def uptime_tuple(self, snmpdump=None): return td.days, td.seconds // 3600, (td.seconds // 60) % 60 _interfaces = None + _wireless_interfaces = None def get_interfaces(self, snmpdump=None): """ @@ -96,6 +98,32 @@ def get_interfaces(self, snmpdump=None): return self._interfaces + def get_wireless_interfaces(self, snmpdump=None): + """ + returns the list of all the wireless interfaces of the device + """ + if self._wireless_interfaces is None: + interfaces = [] + wireless_if_oid = '1.2.840.10036.1.1.1.1.' + interfaces_oid = '1.3.6.1.2.1.2.2.1.2.' + + for i in self._value_to_retrieve(snmpdump=snmpdump): + try: + value_to_get1 = self.get_value( + wireless_if_oid + str(i), snmpdump=snmpdump + ) + + if value_to_get1: + interfaces.append( + self.get_value(interfaces_oid + str(i), snmpdump=snmpdump) + ) + except (NetEngineError, KeyError): + pass + + self._wireless_interfaces = [_f for _f in interfaces if _f] + + return self._wireless_interfaces + _interfaces_MAC = None def interfaces_MAC(self, snmpdump=None): @@ -320,6 +348,7 @@ def interface_addr_and_mask(self, snmpdump=None): results = {} + # TODO: Add ipv6 addresses for i in range(0, len(interface_ip_address)): a = interface_ip_address[i][0][1].asNumbers() ip_address = '.'.join(str(a[i]) for i in range(0, len(a))) @@ -327,8 +356,11 @@ def interface_addr_and_mask(self, snmpdump=None): netmask = '.'.join(str(b[i]) for i in range(0, len(b))) name = self._interface_dict[int(interface_index[i][0][1])] - - results[name] = {'address': ip_address, 'netmask': netmask} + results[name] = { + 'family': 'ipv4', + 'address': ip_address, + 'mask': netmask, + } self._interface_addr_and_mask = results @@ -339,6 +371,7 @@ def interfaces_to_dict(self, snmpdump=None): Returns an ordered dict with all the information available about the interface """ results = [] + wireless_if = self.get_wireless_interfaces() for i in range(0, len(self.get_interfaces(snmpdump=snmpdump))): logger.info(f'====== {i} ======') @@ -358,11 +391,11 @@ def interfaces_to_dict(self, snmpdump=None): logger.info('... mtu ...') mtu = int(self.interfaces_mtu(snmpdump=snmpdump)[i]['mtu']) logger.info('... if_ip ...') - if_ip = ( - self.interface_addr_and_mask(snmpdump=snmpdump) - .get(name, {}) - .get('address', '') - ) + addr = self.interface_addr_and_mask(snmpdump=snmpdump).get(name) + addresses = [addr] if addr is not None else [] + + if name in wireless_if: + if_type = 'wireless' result = self._dict( { @@ -374,7 +407,7 @@ def interfaces_to_dict(self, snmpdump=None): 'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes, "mtu": mtu, - "ip": if_ip, + "addresses": addresses, }, } ) @@ -435,57 +468,45 @@ def local_time(self, snmpdump=None): def RAM_total(self, snmpdump=None): """ - returns the total RAM of the device + returns the total RAM of the device in bytes """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.1', snmpdump=snmpdump)) + return int(self.get_value('1.3.6.1.4.1.2021.4.5.0', snmpdump=snmpdump)) * 1024 def RAM_shared(self, snmpdump=None): """ - returns the shared RAM of the device + returns the shared RAM of the device in bytes """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.8', snmpdump=snmpdump)) + return int(self.get_value('1.3.6.1.4.1.2021.4.13.0', snmpdump=snmpdump)) * 1024 def RAM_cached(self, snmpdump=None): """ - returns the cached RAM of the device - """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.7', snmpdump=snmpdump)) - - def RAM_used(self, snmpdump=None): - """ - returns the used RAM of the device + returns the cached RAM of the device in bytes """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.1', snmpdump=snmpdump)) + return int(self.get_value('1.3.6.1.4.1.2021.4.15.0', snmpdump=snmpdump)) * 1024 def RAM_free(self, snmpdump=None): """ - returns the free RAM of the device + returns the free RAM of the device in bytes """ - return int( - self.RAM_total(snmpdump=snmpdump) - - (self.RAM_used(snmpdump=snmpdump) - self.RAM_cached(snmpdump=snmpdump)) - ) + return int(self.get_value('1.3.6.1.4.1.2021.4.11.0', snmpdump=snmpdump)) * 1024 - def SWAP_total(self, snmpdump=None): + def RAM_buffered(self, snmpdump=None): """ - returns the total SWAP of the device + returns the buffered RAM of the device in bytes """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.10', snmpdump=snmpdump)) + return int(self.get_value('1.3.6.1.4.1.2021.4.14.0', snmpdump=snmpdump)) * 1024 - def SWAP_used(self, snmpdump=None): + def SWAP_total(self, snmpdump=None): """ - returns the used SWAP of the device + returns the total SWAP of the device in bytes """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.6.10', snmpdump=snmpdump)) + return int(self.get_value('1.3.6.1.4.1.2021.4.3.0', snmpdump=snmpdump)) * 1024 def SWAP_free(self, snmpdump=None): """ - returns the free SWAP of the device + returns the free SWAP of the device in bytes """ - SWAP_free = self.SWAP_total(snmpdump=snmpdump) - self.SWAP_used( - snmpdump=snmpdump - ) - return SWAP_free + return int(self.get_value('1.3.6.1.4.1.2021.4.4.0', snmpdump=snmpdump)) * 1024 def CPU_count(self, snmpdump=None): """ @@ -493,24 +514,35 @@ def CPU_count(self, snmpdump=None): """ return len(self.next('1.3.6.1.2.1.25.3.3.1.2.', snmpdump=snmpdump)[3]) + def load(self, snmpdump=None): + """ + Returns an array with load average values respectively in the last + minute, in the last 5 minutes and in the last 15 minutes + """ + array = self.next('1.3.6.1.4.1.2021.10.1.3.', snmpdump=snmpdump)[3] + one = float(array[0][0][1]) + five = float(array[1][0][1]) + fifteen = float(array[2][0][1]) + return [one, five, fifteen] + def resources_to_dict(self, snmpdump=None): """ returns an ordered dict with hardware resources information """ result = self._dict( { + 'load': self.load(snmpdump=snmpdump), 'cpus': self.CPU_count(snmpdump=snmpdump), 'memory': { 'total': self.RAM_total(snmpdump=snmpdump), 'shared': self.RAM_shared(snmpdump=snmpdump), - 'used': self.RAM_used(snmpdump=snmpdump), 'free': self.RAM_free(snmpdump=snmpdump), 'cached': self.RAM_cached(snmpdump=snmpdump), + 'buffered': self.RAM_buffered(snmpdump=snmpdump), }, 'swap': { 'total': self.SWAP_total(snmpdump=snmpdump), 'free': self.SWAP_free(snmpdump=snmpdump), - 'used': self.SWAP_used(snmpdump=snmpdump), }, } ) @@ -574,12 +606,12 @@ def neighbors(self, snmpdump=None): def to_dict(self, snmpdump=None, autowalk=True): if autowalk: - snmpdump = Trie(self.walk('1.3.6')) + snmpdump = Trie(self.walk('1.2')) result = self._dict( { 'type': 'DeviceMonitoring', 'general': { - 'name': self.name(snmpdump=snmpdump), + 'hostname': self.name(snmpdump=snmpdump), 'uptime': self.uptime(snmpdump=snmpdump), 'local_time': self.local_time(snmpdump=snmpdump), }, diff --git a/tests/static/test-openwrt-snmp-oid.json b/tests/static/test-openwrt-snmp-oid.json index f82cd10..5cefd3d 100644 --- a/tests/static/test-openwrt-snmp-oid.json +++ b/tests/static/test-openwrt-snmp-oid.json @@ -55,5 +55,20 @@ "type": "bytes", "value": "\\x07\\xe5\\x06\\x0b\\x06\\x00\r\\x00+\\x00\\x00" }, - "1.3.6.1.2.1.31.1.1.1.1.5": "br-lan" + "1.3.6.1.2.1.31.1.1.1.1.5": "br-lan", + "1.3.6.1.4.1.2021.10.1.3.1": "0.07", + "1.3.6.1.4.1.2021.10.1.3.2": "0.13", + "1.3.6.1.4.1.2021.10.1.3.3": "0.12", + "1.3.6.1.4.1.2021.4.3.0": "0", + "1.3.6.1.4.1.2021.4.4.0": "0", + "1.3.6.1.4.1.2021.4.5.0": "60012", + "1.3.6.1.4.1.2021.4.6.0": "32932", + "1.3.6.1.4.1.2021.4.11.0": "32932", + "1.3.6.1.4.1.2021.4.12.0": "16000", + "1.3.6.1.4.1.2021.4.13.0": "96", + "1.3.6.1.4.1.2021.4.14.0": "2300", + "1.3.6.1.4.1.2021.4.15.0": "7600", + "1.3.6.1.4.1.2021.4.18.0": "0", + "1.3.6.1.4.1.2021.4.19.0": "0", + "1.2.840.10036.1.1.1.1.1": "wifi1" } diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index bfbe3e9..bc4fa7b 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -160,9 +160,9 @@ def test_load(self): load = self.device.load() self.assertIsInstance(load, list) self.assertEqual(len(load), 3) - self.assertIsInstance(load[0], int) - self.assertIsInstance(load[1], int) - self.assertIsInstance(load[2], int) + self.assertIsInstance(load[0], float) + self.assertIsInstance(load[1], float) + self.assertIsInstance(load[2], float) def tearDown(self): patch.stopall() diff --git a/tests/test_snmp/test_openwrt.py b/tests/test_snmp/test_openwrt.py index c48c26e..b00a20b 100644 --- a/tests/test_snmp/test_openwrt.py +++ b/tests/test_snmp/test_openwrt.py @@ -91,9 +91,6 @@ def test_RAM_shared(self): def test_RAM_cached(self): self.assertIsInstance(self.device.RAM_cached(), int) - def test_RAM_used(self): - self.assertIsInstance(self.device.RAM_used(), int) - def test_RAM_free(self): self.assertIsInstance(self.device.RAM_free(), int) @@ -125,5 +122,13 @@ def test_netjson_compliance(self): validate(instance=device_dict, schema=schema) validate(instance=json.loads(device_json), schema=schema) + def test_load(self): + load = self.device.load() + self.assertIsInstance(load, list) + self.assertEqual(len(load), 3) + self.assertIsInstance(load[0], float) + self.assertIsInstance(load[1], float) + self.assertIsInstance(load[2], float) + def tearDown(self): patch.stopall() diff --git a/tests/utils.py b/tests/utils.py index e0ce095..9f1df4e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -74,6 +74,7 @@ def _get_nextcmd_list(return_value): [[[MockOid('1.3.6.1.2.1.4.35.1.4')]], OctetString('0x040e3cca555f')], [[[MockOid('1.3.6.1.2.1.4.35.1.7')]], 1], ], + '1.3.6.1.4.1.2021.10.1.3.': [[[0, '0.87']], [[0, '0.37']], [[0, '0.14']]], } oid = args[2] return _get_nextcmd_list(res[oid]) From d746f2d91f55c8f9b34374027af1b371d23b8f83 Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 29 Jul 2021 19:46:19 +0530 Subject: [PATCH 17/20] [change] Use Trie instead of Dict in walk function --- netengine/backends/snmp/airos.py | 4 +--- netengine/backends/snmp/base.py | 3 ++- netengine/backends/snmp/openwrt.py | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 06418b0..8b68103 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -9,8 +9,6 @@ import logging from datetime import datetime -from pytrie import StringTrie as Trie - from netengine.exceptions import NetEngineError from .base import SNMP @@ -510,7 +508,7 @@ def resources_to_dict(self, snmpdump=None): def to_dict(self, snmpdump=None, autowalk=True): if autowalk: - snmpdump = Trie(self.walk('1.3.6')) + snmpdump = self.walk('1.3.6') result = self._dict( { 'type': 'DeviceMonitoring', diff --git a/netengine/backends/snmp/base.py b/netengine/backends/snmp/base.py index 8cbaf69..1f6cb9c 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -10,6 +10,7 @@ import netaddr from pysnmp.hlapi import ContextData, ObjectIdentity, ObjectType, SnmpEngine, nextCmd +from pytrie import StringTrie from netengine.backends import BaseBackend from netengine.exceptions import NetEngineError @@ -96,7 +97,7 @@ def _oid(self, oid): return str(oid) def walk(self, oid): - result = dict() + result = StringTrie() for (errorIndication, errorStatus, errorIndex, varBinds) in nextCmd( SnmpEngine(), self.community, diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index ea5a52c..909d1c2 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -12,7 +12,6 @@ import pytz from netaddr import EUI, mac_unix_expanded -from pytrie import StringTrie as Trie from netengine.backends.snmp import SNMP from netengine.exceptions import NetEngineError @@ -606,7 +605,7 @@ def neighbors(self, snmpdump=None): def to_dict(self, snmpdump=None, autowalk=True): if autowalk: - snmpdump = Trie(self.walk('1.2')) + snmpdump = self.walk('1.2') result = self._dict( { 'type': 'DeviceMonitoring', From 56ea55112cbccf10a584abb8291ea41c6f59b750 Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 19 Aug 2021 20:33:28 +0530 Subject: [PATCH 18/20] [docs] Update docs for AirOS and OpenWRT backends --- docs/source/index.rst | 2 +- docs/source/topics/backends/snmp.rst | 149 +++++++++++++++++++++++++++ docs/source/topics/snmp.rst | 50 --------- 3 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 docs/source/topics/backends/snmp.rst delete mode 100644 docs/source/topics/snmp.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 0c70a47..43d7d65 100755 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -62,8 +62,8 @@ Contents: .. toctree:: :maxdepth: 2 + /topics/backends/snmp /topics/usage - /topics/snmp Indices and tables ================== diff --git a/docs/source/topics/backends/snmp.rst b/docs/source/topics/backends/snmp.rst new file mode 100644 index 0000000..3e04f08 --- /dev/null +++ b/docs/source/topics/backends/snmp.rst @@ -0,0 +1,149 @@ + +************ +SNMP backend +************ + +SNMP +==== + +SNMP (Simple Network Management Protocol) is a network protocol very useful for retrieving info from a device. +All the information is retrieved by using codes called MIBs. All MIBs have a tree-like structure, every main information is the root and by adding more detail to the info +the tree gains more depth. +Obviously, by getting the smallest MIB which is "1" or simply " . " one can get all the tree. + +The base SNMP backend contains the following methods (some internal methods are not documented and are subject to change in the future): + ++--------------+------------------------------------------------------------------------------------------------------------------------------------------+ +| **to_dict** | Returns a dict containing monitoring information depending on the type of the device. | +| | It follows the `NetJSON Devicemonitoring `_ spec | ++--------------+------------------------------------------------------------------------------------------------------------------------------------------+ +| **to_json** | Calls the `to_dict` method and returns a JSON string of the dict | ++--------------+------------------------------------------------------------------------------------------------------------------------------------------+ +| **validate** | Checks if connection with the device is working and raises `NetengineError` in case something is wrong | ++--------------+------------------------------------------------------------------------------------------------------------------------------------------+ + +Initializing an SNMP backend class requires the following arguments: + ++---------------+---------------------------------------------------------------------+ +| **host** | Management ip or hostname of the device | ++---------------+---------------------------------------------------------------------+ +| **community** | Community string for the SNMP connection. Default value is 'public' | ++---------------+---------------------------------------------------------------------+ +| **agent** | Agent string for the SNMP connection | ++---------------+---------------------------------------------------------------------+ +| **port** | Port for the SNMP connection. Default value is `161` | ++---------------+---------------------------------------------------------------------+ + +The SNMP backend provides support for 2 firmwares: + * AirOS + * OpenWRT + +.. note:: + + The data collected by Netengine is dependant on the OIDs available on your device. Some proprietary manufacturers may not + provide the same information as others. + +***** +AirOS +***** + +With AirOS, Netengine is able to collect the following information which is returned in the +`NetJSON Devicemonitoring `_ format: + ++------------+------------------------------------------------------------------------------------------------------------+ +| general | - uptime: uptime of the device in seconds | +| | - local_time: local time of the device in timestamp | ++------------+------------------------------------------------------------------------------------------------------------+ +| resources | - load: array containing average load on the cpu in the past minute, 5 minutes and 15 minutes respectively | +| | - memory: | +| | - total: total memory in bytes | +| | - buffered: buffered memory in bytes | +| | - free: free memory in bytes | +| | - cached: cached memory in bytes | +| | - swap: | +| | - total: total swap storage in bytes | +| | - free: free swap storage in bytes | ++------------+------------------------------------------------------------------------------------------------------------+ +| interfaces | Each interface is listed with the following information: | +| | | +| | - name | +| | - type | +| | - statistics: | +| | | +| | - rx_bytes | +| | - tx_bytes | ++------------+------------------------------------------------------------------------------------------------------------+ + +AirOS example +============= + +:: + + from netengine.backends.snmp import AirOS + device = AirOS("10.40.0.130") + device.name + 'RM5PomeziaSNode' + device.uptime_tuple + (121, 0, 5) # a tuple containing device uptime hours, mins and seconds + +We have just called two simple properties on **device**, but we can ask **device** for more specific values or portions of the SNMP tree not included in the API, just type:: + device.next("1.3.6") + +Otherwise, if you want simply a value of the tree just type:: + device.get_value("oid_you_want_to_ask_for") + +To collect the whole json:: + device.to_json() + +******* +OpenWRT +******* + +With OpenWRT, Netengine is able to collect the following information which is returned in the +`NetJSON Devicemonitoring `_ format: + ++------------+----------------------------------------------------------+ +| general | - uptime: uptime of the device in seconds | +| | - local_time: local time of the device in timestamp | ++------------+----------------------------------------------------------+ +| resources | - cpus: number of cpus on the device | +| | - memory: | +| | - total: total memory in bytes | +| | - shared: shared memory in bytes | +| | - used: used memory in bytes | +| | - free: free memory in bytes | +| | - cached: cached memory in bytes | +| | - swap: | +| | - total: total swap storage in bytes | +| | - free: free swap storage in bytes | ++------------+----------------------------------------------------------+ +| interfaces | Each interface is listed with the following information: | +| | | +| | - name: name of the interface (example: "eth0") | +| | - statistics: | +| | | +| | - mac | +| | - type | +| | - up | +| | - rx_bytes | +| | - tx_bytes | +| | - mtu | +| | - addresses: | +| | | +| | - family | +| | - address | +| | - mask | ++------------+----------------------------------------------------------+ +| neighbors | Each neighbor is listed with the following information: | +| | - mac: mac address of the neighbor | +| | - state: state of the neighbor (REACHABLE/STALE/DELAY) | +| | - interface: interface of the neighbor | +| | - ip: ip address of the neighbor | ++------------+----------------------------------------------------------+ + +OpenWRT example +=============== + +The same instructions typed above can be applied to OpenWRT itself, just remember to import the correct firmware by typing:: + + from netengine.backends.snmp import OpenWRT diff --git a/docs/source/topics/snmp.rst b/docs/source/topics/snmp.rst deleted file mode 100644 index e584ac9..0000000 --- a/docs/source/topics/snmp.rst +++ /dev/null @@ -1,50 +0,0 @@ - -************** -SNMP backend -************** - -SNMP -======= - -SNMP (Simple Network Management Protocol) is a network protocol very useful for retrieving info from a device. -All the information is retrieved by using codes called MIBs. All MIBs have a tree-like structure, every main information is the root and by adding more detail to the info -the tree gains more depth. -Obviously, by getting the smallest MIB which is "1" or simply " . " one can get all the tree. - - - - -The SNMP backend provides support for 2 firmwares: - * AirOS - * OpenWRT - - - - -AirOS example -============= - -:: - - from netengine.backends.snmp import AirOS - device = AirOS("10.40.0.130") - device.name - 'RM5PomeziaSNode' - device.uptime_tuple - (121, 0, 5) # a tuple containing device uptime hours, mins and seconds - -We have just called two simple properties on **device**, but we can ask **device** for more specific values or portions of the SNMP tree not included in the API, just type:: - device.next("1.3.6") - -Otherwise, if you want simply a value of the tree just type:: - device.get_value("oid_you_want_to_ask_for") - - - - -OpenWRT example -================ - -The same instructions typed above can be applied to OpenWRT itself, just remember to import the correct firmware by typing:: - - from netengine.backends.snmp import OpenWRT From 4e6f1f1888bad553327d567fd90e978ffce08c53 Mon Sep 17 00:00:00 2001 From: purhan Date: Thu, 19 Aug 2021 20:39:20 +0530 Subject: [PATCH 19/20] [chores] Move OpenWRT interface type to match AirOS --- netengine/backends/snmp/openwrt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netengine/backends/snmp/openwrt.py b/netengine/backends/snmp/openwrt.py index 909d1c2..d145079 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -399,14 +399,14 @@ def interfaces_to_dict(self, snmpdump=None): result = self._dict( { 'name': name, + 'type': if_type, 'statistics': { - "mac": mac_address, - "type": if_type, - "up": up, + 'mac': mac_address, + 'up': up, 'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes, - "mtu": mtu, - "addresses": addresses, + 'mtu': mtu, + 'addresses': addresses, }, } ) From 5424532015231d8bd311e8282dca74506dc485ab Mon Sep 17 00:00:00 2001 From: purhan Date: Mon, 23 Aug 2021 09:57:38 +0530 Subject: [PATCH 20/20] [change] Add CPU_count to airOS --- netengine/backends/snmp/airos.py | 15 +++++++++++++++ tests/static/test-airos-snmp.json | 3 ++- tests/test_snmp/test_airos.py | 3 +++ tests/utils.py | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/netengine/backends/snmp/airos.py b/netengine/backends/snmp/airos.py index 8b68103..2c974ff 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -485,6 +485,20 @@ def SWAP_free(self, snmpdump=None): free = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.2.0', snmpdump=snmpdump) return int(free * 1024) + def CPU_count(self, snmpdump=None): + """ + Returns the number of CPU cores for which load information is returned + """ + loadEntry = len( + self.next('1.3.6.1.4.1.10002.1.1.1.4.2.1.', snmpdump=snmpdump)[3] + ) + loadNumber = int( + self.get_value('1.3.6.1.4.1.10002.1.1.1.4.1.0', snmpdump=snmpdump) + ) + # for each loadNumber, 3 types of load values + # are returned: loadIndex, loadDescr, loadValue + return int(loadEntry // (3 * loadNumber)) + def resources_to_dict(self, snmpdump=None): """ returns an ordered dict with hardware resources information @@ -492,6 +506,7 @@ def resources_to_dict(self, snmpdump=None): result = self._dict( { 'load': self.load(snmpdump=snmpdump), + 'cpus': self.CPU_count(snmpdump=snmpdump), 'memory': { 'total': self.RAM_total(snmpdump=snmpdump), 'buffered': self.RAM_buffered(snmpdump=snmpdump), diff --git a/tests/static/test-airos-snmp.json b/tests/static/test-airos-snmp.json index 2f6f0ec..0025db5 100644 --- a/tests/static/test-airos-snmp.json +++ b/tests/static/test-airos-snmp.json @@ -54,5 +54,6 @@ "1.3.6.1.4.1.10002.1.1.1.4.2.1.3.2": "69", "1.3.6.1.4.1.10002.1.1.1.4.2.1.3.3": "32", "1.3.6.1.4.1.10002.1.1.1.2.1.0": "0", - "1.3.6.1.4.1.10002.1.1.1.2.2.0": "0" + "1.3.6.1.4.1.10002.1.1.1.2.2.0": "0", + "1.3.6.1.4.1.10002.1.1.1.4.1.0": "3" } diff --git a/tests/test_snmp/test_airos.py b/tests/test_snmp/test_airos.py index bc4fa7b..227998b 100644 --- a/tests/test_snmp/test_airos.py +++ b/tests/test_snmp/test_airos.py @@ -153,6 +153,9 @@ def test_SWAP_total(self): 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_local_time(self): self.assertIsInstance(self.device.local_time(), int) diff --git a/tests/utils.py b/tests/utils.py index 9f1df4e..7e2b751 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -75,6 +75,7 @@ def _get_nextcmd_list(return_value): [[[MockOid('1.3.6.1.2.1.4.35.1.7')]], 1], ], '1.3.6.1.4.1.2021.10.1.3.': [[[0, '0.87']], [[0, '0.37']], [[0, '0.14']]], + '1.3.6.1.4.1.10002.1.1.1.4.2.1.': [0, 0, 0, [[0, 0, 0] * 3]], } oid = args[2] return _get_nextcmd_list(res[oid])