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 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/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..2c974ff 100644 --- a/netengine/backends/snmp/airos.py +++ b/netengine/backends/snmp/airos.py @@ -7,7 +7,9 @@ import binascii import logging -from datetime import timedelta +from datetime import datetime + +from netengine.exceptions import NetEngineError from .base import SNMP @@ -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,46 +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 - - @property - def uptime_tuple(self): - """ - returns (days, hours, minutes) - """ - td = timedelta(seconds=self.uptime) - - return td.days, td.seconds // 3600, (td.seconds // 60) % 60 + 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 """ @@ -124,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 @@ -135,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 """ @@ -147,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) @@ -162,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) """ @@ -173,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) @@ -197,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 """ @@ -207,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) @@ -222,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) """ @@ -233,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) @@ -248,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 """ @@ -258,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 @@ -270,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, } ) @@ -281,23 +278,31 @@ def interfaces_MAC(self): return self._interfaces_MAC _interfaces_type = None + _wireless_interfaces = None - @property - def interfaces_type(self): + 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.' - 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.get( + self.get_value(types_oid + str(i), snmpdump=snmpdump), + 'unknown', + ), } ) results.append(result) @@ -306,48 +311,83 @@ def interfaces_type(self): return self._interfaces_type - @property - def interfaces_to_dict(self): + 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 = [] - for i in range(0, len(self.get_interfaces())): + 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[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']), + 'name': name, + "type": if_type, + 'statistics': {'rx_bytes': rx_bytes, 'tx_bytes': tx_bytes}, } ) 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 = [] @@ -384,39 +424,115 @@ def wireless_links(self): final.append(result) return final - @property - def RAM_total(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', snmpdump=snmpdump)[3][0][1]) + timestamp = int(datetime.strptime(epoch, '%Y-%m-%d %H:%M:%S').timestamp()) + return timestamp + + def RAM_total(self, snmpdump=None): + """ + 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) * 1024 + + def RAM_free(self, snmpdump=None): """ - Returns the total RAM of the device + Returns the free RAM of the device in bytes """ - total = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.1.0') - return int(total) + free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0', snmpdump=snmpdump) + return int(free) * 1024 - @property - def RAM_free(self): + def RAM_buffered(self, snmpdump=None): """ - Returns the free 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) * 1024 + + def RAM_cached(self, snmpdump=None): + """ + 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) * 1024 + + 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.', snmpdump=snmpdump)[3] + 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 in bytes + """ + total = self.get_value('1.3.6.1.4.1.10002.1.1.1.2.1.0', snmpdump=snmpdump) + return int(total * 1024) + + def SWAP_free(self, snmpdump=None): """ - free = self.get_value('1.3.6.1.4.1.10002.1.1.1.1.2.0') - return int(free) + 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 * 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 + """ + 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), + '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), + }, + } + ) + return result - def to_dict(self): - return self._dict( + def to_dict(self, snmpdump=None, autowalk=True): + if autowalk: + snmpdump = self.walk('1.3.6') + result = 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, - 'interfaces': self.interfaces_to_dict, - 'antennas': [], - 'wireless_dbm': self.wireless_dbm, - 'wireless_links': self.wireless_links, - 'routing_protocols': None, + 'type': 'DeviceMonitoring', + '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 64f21d7..1f6cb9c 100644 --- a/netengine/backends/snmp/base.py +++ b/netengine/backends/snmp/base.py @@ -5,8 +5,13 @@ 'pysnmp library is not installed, install it with "pip install pysnmp"' ) +import binascii import logging +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 @@ -45,6 +50,33 @@ 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 != '': + mac_address = ':'.join( + [mac_address[slice(i, i + 2)] for i in range(0, 12, 2) if i != ''] + ) + 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)): + # 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)] + ) + ) + return res + def _oid(self, oid): """ returns valid oid value to be passed to getCmd() or nextCmd() @@ -64,7 +96,26 @@ def _oid(self, oid): # ensure is string (could be unicode) return str(oid) - def get(self, oid): + def walk(self, oid): + result = StringTrie() + 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 @@ -76,10 +127,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 @@ -91,21 +145,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 """ @@ -116,7 +175,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 2e21adb..d145079 100644 --- a/netengine/backends/snmp/openwrt.py +++ b/netengine/backends/snmp/openwrt.py @@ -5,11 +5,16 @@ __all__ = ['OpenWRT'] -import binascii +import datetime import logging +import struct from datetime import timedelta +import pytz +from netaddr import EUI, mac_unix_expanded + from netengine.backends.snmp import SNMP +from netengine.exceptions import NetEngineError logger = logging.getLogger(__name__) @@ -19,62 +24,62 @@ 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.1.0', snmpdump=snmpdump).split()[1] - @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 + _wireless_interfaces = None - def get_interfaces(self): + def get_interfaces(self, snmpdump=None): """ returns the list of all the interfaces of the device """ @@ -82,44 +87,66 @@ 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] 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 - @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 = 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 != '' - ] - ) - ) - for i in range(0, len(self.get_interfaces())): + mac_string = self._octet_to_mac(i) + mac_trans.append(mac_string) + 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) @@ -129,8 +156,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 """ @@ -141,11 +167,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) @@ -156,8 +182,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 """ @@ -178,7 +203,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 == '': @@ -193,7 +218,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,46 +230,43 @@ def interfaces_speed(self): return self._interfaces_speed - _interfaces_state = None + _interfaces_up = None - @property - def interfaces_state(self): + def interfaces_up(self, snmpdump=None): """ - 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.' 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: - 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) - elif 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), snmpdump=snmpdump + ), + 'up': int( + self.get_value(operative + str(i), snmpdump=snmpdump) + ) + == 1, + } + ) + results.append(result) + elif self.get_value(starting + str(i), snmpdump=snmpdump) == '': 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 - @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) """ @@ -254,12 +276,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) @@ -270,25 +296,28 @@ 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) """ 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.' - for i in self._value_to_retrieve(): + 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)), - 'type': types[self.get_value(types_oid + str(i))], + 'name': self.get_value(starting + str(i), snmpdump=snmpdump), + 'type': types.get( + self.get_value(types_oid + str(i), snmpdump=snmpdump), + 'unknown', + ), } ) results.append(result) @@ -298,23 +327,27 @@ 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 = {} + # 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))) @@ -322,87 +355,268 @@ def interface_addr_and_mask(self): 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 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())): + 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[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']) - logger.info('... state ...') - state = self.interfaces_state[i]['state'] + tx_bytes = int(self.interfaces_bytes(snmpdump=snmpdump)[i]['tx']) + logger.info('... up ...') + up = self.interfaces_up(snmpdump=snmpdump)[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 + mtu = int(self.interfaces_mtu(snmpdump=snmpdump)[i]['mtu']) + logger.info('... if_ip ...') + 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( { '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': { + 'mac': mac_address, + 'up': up, + 'rx_bytes': rx_bytes, + 'tx_bytes': tx_bytes, + 'mtu': mtu, + 'addresses': addresses, + }, } ) results.append(result) return results - @property - def RAM_total(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', 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: + (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() + ) + + def RAM_total(self, snmpdump=None): + """ + returns the total RAM of the device in bytes + """ + 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 in bytes + """ + 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 in bytes + """ + 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 in bytes + """ + return int(self.get_value('1.3.6.1.4.1.2021.4.11.0', snmpdump=snmpdump)) * 1024 + + def RAM_buffered(self, snmpdump=None): + """ + returns the buffered RAM of the device in bytes + """ + return int(self.get_value('1.3.6.1.4.1.2021.4.14.0', snmpdump=snmpdump)) * 1024 + + def SWAP_total(self, snmpdump=None): + """ + returns the total SWAP of the device in bytes + """ + 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 in bytes + """ + return int(self.get_value('1.3.6.1.4.1.2021.4.4.0', snmpdump=snmpdump)) * 1024 + + def CPU_count(self, snmpdump=None): """ - returns the total RAM of the device + returns the count of CPUs of the device """ - return int(self.get_value('1.3.6.1.2.1.25.2.3.1.5.1')) + 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), + '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), + }, + } + ) + return result + + def neighbors(self, snmpdump=None): + """ + returns a dict with neighbors information + """ + states_map = { + '1': 'REACHABLE', + '2': 'STALE', + '3': 'DELAY', + '4': 'PROBE', + '5': 'INVALID', + '6': 'UNKNOWN', + '7': 'INCOMPLETE', + } + + 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.', snmpdump=snmpdump)[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() + 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 + ) + interface_num = neighbor[0][0].getOid()[10] + 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 + result.append( + self._dict( + { + 'mac': str(mac), + 'state': str(state), + 'interface': str(interface), + 'ip': str(ip), + } + ) + ) + return result - def to_dict(self): - return self._dict( + def to_dict(self, snmpdump=None, autowalk=True): + if autowalk: + snmpdump = self.walk('1.2') + result = 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': { + 'hostname': self.name(snmpdump=snmpdump), + '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-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/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/static/test-airos-snmp.json b/tests/static/test-airos-snmp.json index e470631..0025db5 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", @@ -46,5 +46,14 @@ "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", + "1.3.6.1.4.1.10002.1.1.1.4.1.0": "3" } diff --git a/tests/static/test-openwrt-snmp-oid.json b/tests/static/test-openwrt-snmp-oid.json index 2db1140..5cefd3d 100644 --- a/tests/static/test-openwrt-snmp-oid.json +++ b/tests/static/test-openwrt-snmp-oid.json @@ -45,5 +45,30 @@ "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": { + "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.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_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 4ea3227..227998b 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 @@ -26,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, @@ -37,6 +40,7 @@ def setUp(self): ), ) self.getcmd_patcher.start() + self.nextcmd_patcher.start() def test_get_value_error(self): self.getcmd_patcher.stop() @@ -46,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() @@ -64,109 +69,103 @@ def test_get(self): def test_properties(self): device = self.device - device.os - device.name - device.model - device.os - device.uptime - device.uptime_tuple + 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): - 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) + 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) + 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): - 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)) - - def test_manufacturer_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.assertIsNotNone(self.device.to_dict()['manufacturer']) + self.assertTrue(isinstance(self.device.to_dict(autowalk=False), dict)) + + def test_netjson_compliance(self): + 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): - with self.nextcmd_patcher: - 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) + + 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_CPU_count(self): + self.assertIsInstance(self.device.CPU_count(), int) + + def test_local_time(self): + self.assertIsInstance(self.device.local_time(), int) - def test_uptime_tuple(self): - self.assertIsInstance(self.device.uptime_tuple, tuple) + 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/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 f12588e..b00a20b 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 @@ -24,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, @@ -35,76 +38,97 @@ def setUp(self): ), ) self.getcmd_patcher.start() + self.nextcmd_patcher.start() def test_os(self): - self.assertIsInstance(self.device.os, tuple) + 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) + 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): - 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) + 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_state, 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) + 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_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, [[[0, 1]]] * 5]) - 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): - 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']) + 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(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_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 6c3b548..7e2b751 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,10 +1,21 @@ +import codecs import json 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): @@ -32,17 +43,39 @@ 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]]] @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]]] * 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.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.': [ + [[[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']]], + '1.3.6.1.4.1.10002.1.1.1.4.2.1.': [0, 0, 0, [[0, 0, 0] * 3]], } - return return_data[oid] + oid = args[2] + return _get_nextcmd_list(res[oid])