diff --git a/puresnmp/__init__.py b/puresnmp/__init__.py index 5355b58..88ef0e5 100644 --- a/puresnmp/__init__.py +++ b/puresnmp/__init__.py @@ -3,7 +3,15 @@ taken to make this as pythonic as possible and hide as many of the gory implementations as possible. """ + +# TODO (advanced): This module should not make use of it's own functions. The +# module exists as an abstraction layer only. If one function uses a +# "siblng" function, valuable information is lost. In general, this module +# is beginning to be too "thick", containing too much business logic for a +# mere abstraction layer. +from collections import OrderedDict, namedtuple from typing import List, Tuple +import logging from .x690.types import ( Integer, @@ -15,6 +23,7 @@ from .x690.util import tablify from .exc import SnmpError from .pdu import ( + BulkGetRequest, GetNextRequest, GetRequest, SetRequest, @@ -23,6 +32,13 @@ from .const import Version from .transport import send, get_request_id +_set = set + + +BulkResult = namedtuple('BulkResult', 'scalars listing') +WalkRow = namedtuple('WalkRow', 'value unfinished') +LOG = logging.getLogger(__name__) + def get(ip: str, community: str, oid: str, port: int=161): """ @@ -66,7 +82,7 @@ def multiget(ip: str, community: str, oids: List[str], port: int=161): return output -def getnext(ip, community, oid, port): +def getnext(ip, community, oid, port=161): """ Executes a single SNMP GETNEXT request (used inside *walk*). @@ -132,7 +148,62 @@ def walk(ip: str, community: str, oid, port: int=161): return multiwalk(ip, community, [oid], port) -def multiwalk(ip: str, community: str, oids: List[str], port: int=161): +def unzip_walk_result(varbinds, base_ids): + """ + Takes a list of varbinds and a list of base OIDs and returns a mapping from + those base IDs to lists of varbinds. + """ + n = len(base_ids) + results = {} + for i in range(n): + results[base_ids[i]] = varbinds[i::n] + return results + + +def get_unfinished_walk_oids(varbinds, requested_oids, bases=None): + + # split result into a list for each requested base OID + results = unzip_walk_result(varbinds, requested_oids) + + # Sometimes (for continued walk requests), the requested OIDs are actually + # children of the originally requested OIDs on the second and subsequent + # requests. If *bases* is set, it will contain the originally requested OIDs + # and we need to replace the dict keys with the appropriate bases. + if bases: + new_results = {} + for k, v in results.items(): + containment = [base for base in bases if k in base] + if len(containment) > 1: + raise RuntimeError('Unexpected OID result. A value was ' + 'contained in more than one base than ' + 'should be possible!') + if not containment: + continue + new_results[containment[0]] = v + results = new_results + + # we now have a list of values for each requested OID and need to determine + # if we need to continue fetching: Inspect the last item of each list if + # those OIDs are still children of the requested IDs we need to continue + # fetching using *those* IDs (as we're using GetNext behaviour). If they are + # *not* children of the requested OIDs, we went too far (in the case of a + # bulk operation) and need to remove all outliers. + # + # The above behaviour is the same for both bulk and simple operations. For + # simple operations we simply have a list of 1 element per OID, but the + # behaviour is identical + + # Build a mapping from the originally requested OID to the last fetched OID + # from that tree. + last_received_oids = {k: WalkRow(v[-1], v[-1].oid in k) + for k, v in results.items()} + + output = [item for item in last_received_oids.items() if item[1].unfinished] + return output + + +def multiwalk(ip: str, community: str, oids: List[str], port: int=161, + fetcher=multigetnext): """ Executes a sequence of SNMP GETNEXT requests and returns an generator over :py:class:`~puresnmp.pdu.VarBind` instances. @@ -144,31 +215,40 @@ def multiwalk(ip: str, community: str, oids: List[str], port: int=161): >>> walk('127.0.0.1', 'private', ['1.3.6.1.2.1.1', '1.3.6.1.4.1.1']) - """ - - varbinds = multigetnext(ip, community, oids, port) - - retrieved_oids = [str(bind.oid) for bind in varbinds] - prev_retrieved_oids = [] - while retrieved_oids: - for bind in varbinds: - yield bind - - varbinds = multigetnext(ip, community, retrieved_oids, port) - retrieved_oids = [str(bind.oid) for bind in varbinds] - - # ending condition (check if we need to stop the walk) - retrieved_oids_ = [ObjectIdentifier.from_string(_) - for _ in retrieved_oids] - requested_oids = [ObjectIdentifier.from_string(_) - for _ in oids] - contained_oids = [ - a in b for a, b in zip(retrieved_oids_, requested_oids)] - if not all(contained_oids) or retrieved_oids == prev_retrieved_oids: - return - - prev_retrieved_oids = retrieved_oids + LOG.debug('Walking on %d OIDs using %s', len(oids), fetcher.__name__) + + varbinds = fetcher(ip, community, oids, port) + requested_oids = [ObjectIdentifier.from_string(oid) for oid in oids] + unfinished_oids = get_unfinished_walk_oids(varbinds, requested_oids) + LOG.debug('%d of %d OIDs need to be continued', + len(unfinished_oids), + len(oids)) + output = unzip_walk_result(varbinds, requested_oids) + + # As long as we have unfinished OIDs, we need to continue the walk for + # those. + while unfinished_oids: + next_fetches = [_[1].value.oid for _ in unfinished_oids] + varbinds = fetcher(ip, community, [str(_) for _ in next_fetches], port) + unfinished_oids = get_unfinished_walk_oids(varbinds, next_fetches, + bases=requested_oids) + LOG.debug('%d of %d OIDs need to be continued', + len(unfinished_oids), + len(oids)) + for k, v in unzip_walk_result(varbinds, next_fetches).items(): + for ko, vo in output.items(): + if k in ko: + vo.extend(v) + + yielded = _set([]) + for v in output.values(): + for varbind in v: + containment = [varbind.oid in _ for _ in requested_oids] + if not any(containment) or varbind.oid in yielded: + continue + yielded.add(varbind.oid) + yield varbind def set(ip: str, community: str, oid: str, value: Type, port: int=161): @@ -224,6 +304,171 @@ def multiset(ip: str, community: str, mappings: List[Tuple[str, Type]], return output +def bulkget(ip, community, scalar_oids, repeating_oids, max_list_size=1, + port=161): + """ + Runs a "bulk" get operation and returns a :py:class:`~.BulkResult` instance. + This contains both a mapping for the scalar variables (the "non-repeaters") + and an OrderedDict instance containing the remaining list (the "repeaters"). + + The OrderedDict is ordered the same way as the SNMP response (whatever the + remote device returns). + + This operation can retrieve both single/scalar values *and* lists of values + ("repeating values") in one single request. You can for example retrieve the + hostname (a scalar value), the list of interfaces (a repeating value) and + the list of physical entities (another repeating value) in one single + request. + + Note that this behaves like a **getnext** request for scalar values! So you + will receive the value of the OID which is *immediately following* the OID + you specified for both scalar and repeating values! + + :param scalar_oids: contains the OIDs that should be fetched as single + value. + :param repeating_oids: contains the OIDs that should be fetched as list. + :param max_list_size: defines the max length of each list. + + Example:: + + >>> ip = '192.168.1.1' + >>> community = 'private' + >>> result = bulkget(ip, + ... community, + ... scalar_oids=['1.3.6.1.2.1.1.1', + ... '1.3.6.1.2.1.1.2'], + ... repeating_oids=['1.3.6.1.2.1.3.1', + ... '1.3.6.1.2.1.5.1'], + ... max_list_size=10) + BulkResult( + scalars={'1.3.6.1.2.1.1.2.0': '1.3.6.1.4.1.8072.3.2.10', + '1.3.6.1.2.1.1.1.0': b'Linux aafa4dce0ad4 4.4.0-28-' + b'generic #47-Ubuntu SMP Fri Jun 24 ' + b'10:09:13 UTC 2016 x86_64'}, + listing=OrderedDict([ + ('1.3.6.1.2.1.3.1.1.1.10.1.172.17.0.1', 10), + ('1.3.6.1.2.1.5.1.0', b'\x01'), + ('1.3.6.1.2.1.3.1.1.2.10.1.172.17.0.1', b'\x02B\x8e>\x9ee'), + ('1.3.6.1.2.1.5.2.0', b'\x00'), + ('1.3.6.1.2.1.3.1.1.3.10.1.172.17.0.1', b'\xac\x11\x00\x01'), + ('1.3.6.1.2.1.5.3.0', b'\x00'), + ('1.3.6.1.2.1.4.1.0', 1), + ('1.3.6.1.2.1.5.4.0', b'\x01'), + ('1.3.6.1.2.1.4.3.0', b'\x00\xb1'), + ('1.3.6.1.2.1.5.5.0', b'\x00'), + ('1.3.6.1.2.1.4.4.0', b'\x00'), + ('1.3.6.1.2.1.5.6.0', b'\x00'), + ('1.3.6.1.2.1.4.5.0', b'\x00'), + ('1.3.6.1.2.1.5.7.0', b'\x00'), + ('1.3.6.1.2.1.4.6.0', b'\x00'), + ('1.3.6.1.2.1.5.8.0', b'\x00'), + ('1.3.6.1.2.1.4.7.0', b'\x00'), + ('1.3.6.1.2.1.5.9.0', b'\x00'), + ('1.3.6.1.2.1.4.8.0', b'\x00'), + ('1.3.6.1.2.1.5.10.0', b'\x00')])) + """ + + scalar_oids = scalar_oids or [] # protect against empty values + repeating_oids = repeating_oids or [] # protect against empty values + + oids = [ + ObjectIdentifier.from_string(oid) for oid in scalar_oids + ] + [ + ObjectIdentifier.from_string(oid) for oid in repeating_oids + ] + + non_repeaters = len(scalar_oids) + + packet = Sequence( + Integer(Version.V2C), + OctetString(community), + BulkGetRequest(get_request_id(), non_repeaters, max_list_size, *oids) + ) + + response = send(ip, port, bytes(packet)) + raw_response = Sequence.from_bytes(response) + + # See RFC=3416 for details of the following calculation + n = min(non_repeaters, len(oids)) + m = max_list_size + r = max(len(oids) - n, 0) + expected_max_varbinds = n + (m * r) + + if len(raw_response[2].varbinds) > expected_max_varbinds: + raise SnmpError('Unexpected response. Expected no more than %d ' + 'varbinds, but got %d!' % ( + expected_max_varbinds, len(oids))) + + # cut off the scalar OIDs from the listing(s) + scalar_tmp = raw_response[2].varbinds[0:len(scalar_oids)] + repeating_tmp = raw_response[2].varbinds[len(scalar_oids):] + + # prepare output for scalar OIDs + scalar_out = {str(oid): value.pythonize() for oid, value in scalar_tmp} + + # prepare output for listing + repeating_out = OrderedDict() + for oid, value in repeating_tmp: + repeating_out[str(oid)] = value.pythonize() + + return BulkResult(scalar_out, repeating_out) + + +def bulkwalk_fetcher(bulk_size=10): + """ + Create a bulk fetcher with a fixed limit on "repeatable" OIDs. + """ + def fun(ip, community, oids, port=161): + result = bulkget(ip, community, [], oids, max_list_size=bulk_size, + port=port) + return [VarBind(ObjectIdentifier.from_string(k), v) + for k, v in result.listing.items()] + fun.__name__ = 'bulkwalk_fetcher(%d)' % bulk_size + return fun + + +def bulkwalk(ip, community, oids, bulk_size=10, port=161): + """ + More efficient implementation of :py:fun:`~.walk`. It uses + :py:fun:`~.bulkget` under the hood instead of :py:fun:`~.getnext`. + + Just like :py:fun:`~.multiwalk`, it returns a generator over + :py:class:`~puresnmp.pdu.VarBind` instances. + + :param ip: The IP address of the target host. + :param community: The community string for the SNMP connection. + :param oids: A list of base OIDs to use in the walk operation. + :param bulk_size: How many varbinds to request from the remote host with + one request. + :param port: The TCP port of the remote host. + + Example:: + + >>> from puresnmp import bulkwalk + >>> ip = '127.0.0.1' + >>> community = 'private' + >>> oids = [ + ... '1.3.6.1.2.1.2.2.1.2', # name + ... '1.3.6.1.2.1.2.2.1.6', # MAC + ... '1.3.6.1.2.1.2.2.1.22', # ? + ... ] + >>> result = bulkwalk(ip, community, oids) + >>> for row in result: + ... print(row) + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1)), value=b'lo') + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 6, 1)), value=b'') + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 22, 1)), value='0.0') + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 38)), value=b'eth0') + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 6, 38)), value=b'\x02B\xac\x11\x00\x02') + VarBind(oid=ObjectIdentifier((1, 3, 6, 1, 2, 1, 2, 2, 1, 22, 38)), value='0.0') + """ + + result = multiwalk(ip, community, oids, port=161, + fetcher=bulkwalk_fetcher(bulk_size)) + for oid, value in result: + yield VarBind(oid, value) + + def table(ip: str, community: str, oid: str, port: int=161, num_base_nodes: int=0): """ diff --git a/puresnmp/pdu.py b/puresnmp/pdu.py index f6b23cc..2a4e322 100644 --- a/puresnmp/pdu.py +++ b/puresnmp/pdu.py @@ -26,7 +26,16 @@ from .x690.util import TypeInfo -VarBind = namedtuple('VarBind', 'oid, value') +class VarBind(namedtuple('VarBind', 'oid, value')): + + def __new__(cls, oid, value): + if not isinstance(oid, (ObjectIdentifier, str)): + raise TypeError('OIDs for VarBinds must be ObjectIdentifier or str' + ' instances!') + if isinstance(oid, str): + oid = ObjectIdentifier.from_string(oid) + return super().__new__(cls, oid, value) + # TODO (trivial) raise an error if more than MAX_VARBINDS are used in a request. MAX_VARBINDS = 2147483647 # Defined in RFC 3416 @@ -190,3 +199,86 @@ class SetRequest(SnmpMessage): Represents an SNMP SET Request. """ TAG = 3 + + +class BulkGetRequest(Type): + """ + Represents a SNMP GetBulk request + """ + TYPECLASS = TypeInfo.CONTEXT + TAG = 5 + + @classmethod + def decode(cls, data): + """ + This method takes a :py:class:`bytes` object and converts it to + an application object. + """ + # TODO (advanced): recent tests revealed that this is *not symmetric* + # with __bytes__ of this class. This should be ensured! + if not data: + raise EmptyMessage('No data to decode!') + request_id, data = pop_tlv(data) + non_repeaters, data = pop_tlv(data) + max_repeaters, data = pop_tlv(data) + values, data = pop_tlv(data) + + oids = [str(*oid) for oid, _ in values] + + return cls( + request_id, + non_repeaters, + max_repeaters, + *oids + ) + + def __init__(self, request_id, non_repeaters, max_repeaters, *oids): + self.request_id = request_id + self.non_repeaters = non_repeaters + self.max_repeaters = max_repeaters + self.varbinds = [] + for oid in oids: + self.varbinds.append(VarBind(oid, Null())) + + def __bytes__(self): + wrapped_varbinds = [Sequence(vb.oid, vb.value) for vb in self.varbinds] + data = [ + Integer(self.request_id), + Integer(self.non_repeaters), + Integer(self.max_repeaters), + Sequence(*wrapped_varbinds) + ] + payload = b''.join([bytes(chunk) for chunk in data]) + + tinfo = TypeInfo(TypeInfo.CONTEXT, TypeInfo.CONSTRUCTED, self.TAG) + length = encode_length(len(payload)) + return bytes(tinfo) + length + payload + + def __repr__(self): + return '%s(%r, %r)' % ( + self.__class__.__name__, + self.request_id, self.varbinds) + + def __eq__(self, other): + # pylint: disable=unidiomatic-typecheck + return (type(other) == type(self) and + self.request_id == other.request_id and + self.non_repeaters == other.non_repeaters and + self.max_repeaters == other.max_repeaters and + self.varbinds == other.varbinds) + + def pretty(self) -> str: # pragma: no cover + """ + Returns a "prettified" string representing the SNMP message. + """ + lines = [ + self.__class__.__name__, + ' Request ID: %s' % self.request_id, + ' Non Repeaters: %s' % self.non_repeaters, + ' Max Repeaters: %s' % self.max_repeaters, + ' Varbinds: ', + ] + for bind in self.varbinds: + lines.append(' %s: %s' % (bind.oid, bind.value)) + + return '\n'.join(lines) diff --git a/puresnmp/test/data/bulk_get_request.hex b/puresnmp/test/data/bulk_get_request.hex new file mode 100644 index 0000000..0ff6646 --- /dev/null +++ b/puresnmp/test/data/bulk_get_request.hex @@ -0,0 +1,4 @@ +0000: 30 37 02 01 01 04 06 70 75 62 6C 69 63 A5 2A 02 07.....public.*. +0016: 04 1A 12 02 6A 02 01 00 02 01 05 30 1C 30 0C 06 ....j......0.0.. +0032: 08 2B 06 01 02 01 02 02 00 05 00 30 0C 06 08 2B .+.........0...+ +0048: 06 01 02 01 02 03 00 05 00 ......... diff --git a/puresnmp/test/data/bulk_get_response.hex b/puresnmp/test/data/bulk_get_response.hex new file mode 100644 index 0000000..2e8c8d1 --- /dev/null +++ b/puresnmp/test/data/bulk_get_response.hex @@ -0,0 +1,15 @@ +0000: 30 81 EC 02 01 01 04 06 70 75 62 6C 69 63 A2 81 0.......public.. +0016: DE 02 04 41 E7 06 ED 02 01 00 02 01 00 30 81 CF ...A.........0.. +0032: 30 62 06 08 2B 06 01 02 01 01 01 00 04 56 4C 69 0b..+........VLi +0048: 6E 75 78 20 37 65 36 38 65 36 30 66 65 33 30 33 nux 7e68e60fe303 +0064: 20 34 2E 34 2E 30 2D 32 38 2D 67 65 6E 65 72 69 4.4.0-28-generi +0080: 63 20 23 34 37 2D 55 62 75 6E 74 75 20 53 4D 50 c #47-Ubuntu SMP +0096: 20 46 72 69 20 4A 75 6E 20 32 34 20 31 30 3A 30 Fri Jun 24 10:0 +0112: 39 3A 31 33 20 55 54 43 20 32 30 31 36 20 78 38 9:13 UTC 2016 x8 +0128: 36 5F 36 34 30 15 06 10 2B 06 01 02 01 03 01 01 6_640...+....... +0144: 01 0A 01 81 2C 11 00 01 02 01 0A 30 1A 06 10 2B ....,......0...+ +0160: 06 01 02 01 03 01 01 02 0A 01 81 2C 11 00 01 04 ...........,.... +0176: 06 02 42 E2 C5 8D 09 30 18 06 10 2B 06 01 02 01 ..B....0...+.... +0192: 03 01 01 03 0A 01 81 2C 11 00 01 40 04 AC 11 00 .......,...@.... +0208: 01 30 0D 06 08 2B 06 01 02 01 04 01 00 02 01 01 .0...+.......... +0224: 30 0D 06 08 2B 06 01 02 01 04 03 00 41 01 39 0...+.......A.9 diff --git a/puresnmp/test/data/bulkwalk_request_1.hex b/puresnmp/test/data/bulkwalk_request_1.hex new file mode 100644 index 0000000..74fea1c --- /dev/null +++ b/puresnmp/test/data/bulkwalk_request_1.hex @@ -0,0 +1,3 @@ +0000: 30 29 02 01 01 04 07 70 72 69 76 61 74 65 A5 1B 0).....private.. +0016: 02 04 3B B3 67 A6 02 01 00 02 01 14 30 0D 30 0B ..;.g.......0.0. +0032: 06 07 2B 06 01 02 01 02 02 05 00 ..+........ diff --git a/puresnmp/test/data/bulkwalk_request_2.hex b/puresnmp/test/data/bulkwalk_request_2.hex new file mode 100644 index 0000000..276dc8c --- /dev/null +++ b/puresnmp/test/data/bulkwalk_request_2.hex @@ -0,0 +1,3 @@ +0000: 30 2C 02 01 01 04 07 70 72 69 76 61 74 65 A5 1E 0,.....private.. +0016: 02 04 3B B3 67 A7 02 01 00 02 01 14 30 10 30 0E ..;.g.......0.0. +0032: 06 0A 2B 06 01 02 01 02 02 01 0A 0A 05 00 ..+........... diff --git a/puresnmp/test/data/bulkwalk_request_3.hex b/puresnmp/test/data/bulkwalk_request_3.hex new file mode 100644 index 0000000..0a70e5f --- /dev/null +++ b/puresnmp/test/data/bulkwalk_request_3.hex @@ -0,0 +1,3 @@ +0000: 30 2C 02 01 01 04 07 70 72 69 76 61 74 65 A5 1E 0,.....private.. +0016: 02 04 3B B3 67 A8 02 01 00 02 01 14 30 10 30 0E ..;.g.......0.0. +0032: 06 0A 2B 06 01 02 01 02 02 01 14 0A 05 00 ..+........... diff --git a/puresnmp/test/data/bulkwalk_response_1.hex b/puresnmp/test/data/bulkwalk_response_1.hex new file mode 100644 index 0000000..69a3c29 --- /dev/null +++ b/puresnmp/test/data/bulkwalk_response_1.hex @@ -0,0 +1,25 @@ +0000: 30 82 01 89 02 01 01 04 07 70 72 69 76 61 74 65 0........private +0016: A2 82 01 79 02 04 3B B3 67 A6 02 01 00 02 01 00 ...y..;.g....... +0032: 30 82 01 69 30 0F 06 0A 2B 06 01 02 01 02 02 01 0..i0...+....... +0048: 01 01 02 01 01 30 0F 06 0A 2B 06 01 02 01 02 02 .....0...+...... +0064: 01 01 0A 02 01 0A 30 10 06 0A 2B 06 01 02 01 02 ......0...+..... +0080: 02 01 02 01 04 02 6C 6F 30 12 06 0A 2B 06 01 02 ......lo0...+... +0096: 01 02 02 01 02 0A 04 04 65 74 68 30 30 0F 06 0A ........eth00... +0112: 2B 06 01 02 01 02 02 01 03 01 02 01 18 30 0F 06 +............0.. +0128: 0A 2B 06 01 02 01 02 02 01 03 0A 02 01 06 30 11 .+............0. +0144: 06 0A 2B 06 01 02 01 02 02 01 04 01 02 03 01 00 ..+............. +0160: 00 30 10 06 0A 2B 06 01 02 01 02 02 01 04 0A 02 .0...+.......... +0176: 02 05 DC 30 12 06 0A 2B 06 01 02 01 02 02 01 05 ...0...+........ +0192: 01 42 04 00 98 96 80 30 13 06 0A 2B 06 01 02 01 .B.....0...+.... +0208: 02 02 01 05 0A 42 05 00 FF FF FF FF 30 0E 06 0A .....B......0... +0224: 2B 06 01 02 01 02 02 01 06 01 04 00 30 14 06 0A +...........0... +0240: 2B 06 01 02 01 02 02 01 06 0A 04 06 02 42 AC 11 +............B.. +0256: 00 02 30 0F 06 0A 2B 06 01 02 01 02 02 01 07 01 ..0...+......... +0272: 02 01 01 30 0F 06 0A 2B 06 01 02 01 02 02 01 07 ...0...+........ +0288: 0A 02 01 01 30 0F 06 0A 2B 06 01 02 01 02 02 01 ....0...+....... +0304: 08 01 02 01 01 30 0F 06 0A 2B 06 01 02 01 02 02 .....0...+...... +0320: 01 08 0A 02 01 01 30 0F 06 0A 2B 06 01 02 01 02 ......0...+..... +0336: 02 01 09 01 43 01 00 30 0F 06 0A 2B 06 01 02 01 ....C..0...+.... +0352: 02 02 01 09 0A 43 01 00 30 10 06 0A 2B 06 01 02 .....C..0...+... +0368: 01 02 02 01 0A 01 41 02 00 AC 30 11 06 0A 2B 06 ......A...0...+. +0384: 01 02 01 02 02 01 0A 0A 41 03 00 EC 8E ........A.... diff --git a/puresnmp/test/data/bulkwalk_response_2.hex b/puresnmp/test/data/bulkwalk_response_2.hex new file mode 100644 index 0000000..133646d --- /dev/null +++ b/puresnmp/test/data/bulkwalk_response_2.hex @@ -0,0 +1,24 @@ +0000: 30 82 01 79 02 01 01 04 07 70 72 69 76 61 74 65 0..y.....private +0016: A2 82 01 69 02 04 3B B3 67 A7 02 01 00 02 01 00 ...i..;.g....... +0032: 30 82 01 59 30 0F 06 0A 2B 06 01 02 01 02 02 01 0..Y0...+....... +0048: 0B 01 41 01 02 30 10 06 0A 2B 06 01 02 01 02 02 ..A..0...+...... +0064: 01 0B 0A 41 02 02 34 30 0F 06 0A 2B 06 01 02 01 ...A..40...+.... +0080: 02 02 01 0C 01 41 01 00 30 0F 06 0A 2B 06 01 02 .....A..0...+... +0096: 01 02 02 01 0C 0A 41 01 00 30 0F 06 0A 2B 06 01 ......A..0...+.. +0112: 02 01 02 02 01 0D 01 41 01 00 30 0F 06 0A 2B 06 .......A..0...+. +0128: 01 02 01 02 02 01 0D 0A 41 01 00 30 0F 06 0A 2B ........A..0...+ +0144: 06 01 02 01 02 02 01 0E 01 41 01 00 30 0F 06 0A .........A..0... +0160: 2B 06 01 02 01 02 02 01 0E 0A 41 01 00 30 0F 06 +.........A..0.. +0176: 0A 2B 06 01 02 01 02 02 01 0F 01 41 01 00 30 0F .+.........A..0. +0192: 06 0A 2B 06 01 02 01 02 02 01 0F 0A 41 01 00 30 ..+.........A..0 +0208: 10 06 0A 2B 06 01 02 01 02 02 01 10 01 41 02 00 ...+.........A.. +0224: AC 30 11 06 0A 2B 06 01 02 01 02 02 01 10 0A 41 .0...+.........A +0240: 03 00 AD 07 30 0F 06 0A 2B 06 01 02 01 02 02 01 ....0...+....... +0256: 11 01 41 01 02 30 10 06 0A 2B 06 01 02 01 02 02 ..A..0...+...... +0272: 01 11 0A 41 02 01 BA 30 0F 06 0A 2B 06 01 02 01 ...A...0...+.... +0288: 02 02 01 12 01 41 01 00 30 0F 06 0A 2B 06 01 02 .....A..0...+... +0304: 01 02 02 01 12 0A 41 01 00 30 0F 06 0A 2B 06 01 ......A..0...+.. +0320: 02 01 02 02 01 13 01 41 01 00 30 0F 06 0A 2B 06 .......A..0...+. +0336: 01 02 01 02 02 01 13 0A 41 01 00 30 0F 06 0A 2B ........A..0...+ +0352: 06 01 02 01 02 02 01 14 01 41 01 00 30 0F 06 0A .........A..0... +0368: 2B 06 01 02 01 02 02 01 14 0A 41 01 00 +.........A.. diff --git a/puresnmp/test/data/bulkwalk_response_3.hex b/puresnmp/test/data/bulkwalk_response_3.hex new file mode 100644 index 0000000..3a3312d --- /dev/null +++ b/puresnmp/test/data/bulkwalk_response_3.hex @@ -0,0 +1,24 @@ +0000: 30 82 01 77 02 01 01 04 07 70 72 69 76 61 74 65 0..w.....private +0016: A2 82 01 67 02 04 3B B3 67 A8 02 01 00 02 01 00 ...g..;.g....... +0032: 30 82 01 57 30 0F 06 0A 2B 06 01 02 01 02 02 01 0..W0...+....... +0048: 15 01 42 01 00 30 0F 06 0A 2B 06 01 02 01 02 02 ..B..0...+...... +0064: 01 15 0A 42 01 00 30 0F 06 0A 2B 06 01 02 01 02 ...B..0...+..... +0080: 02 01 16 01 06 01 00 30 0F 06 0A 2B 06 01 02 01 .......0...+.... +0096: 02 02 01 16 0A 06 01 00 30 15 06 10 2B 06 01 02 ........0...+... +0112: 01 03 01 01 01 0A 01 81 2C 11 00 01 02 01 0A 30 ........,......0 +0128: 1A 06 10 2B 06 01 02 01 03 01 01 02 0A 01 81 2C ...+..........., +0144: 11 00 01 04 06 02 42 E2 C5 8D 09 30 18 06 10 2B ......B....0...+ +0160: 06 01 02 01 03 01 01 03 0A 01 81 2C 11 00 01 40 ...........,...@ +0176: 04 AC 11 00 01 30 0D 06 08 2B 06 01 02 01 04 01 .....0...+...... +0192: 00 02 01 01 30 0E 06 08 2B 06 01 02 01 04 03 00 ....0...+....... +0208: 41 02 01 EE 30 0D 06 08 2B 06 01 02 01 04 04 00 A...0...+....... +0224: 41 01 00 30 0D 06 08 2B 06 01 02 01 04 05 00 41 A..0...+.......A +0240: 01 00 30 0D 06 08 2B 06 01 02 01 04 06 00 41 01 ..0...+.......A. +0256: 00 30 0D 06 08 2B 06 01 02 01 04 07 00 41 01 00 .0...+.......A.. +0272: 30 0D 06 08 2B 06 01 02 01 04 08 00 41 01 00 30 0...+.......A..0 +0288: 0E 06 08 2B 06 01 02 01 04 09 00 41 02 01 E0 30 ...+.......A...0 +0304: 0E 06 08 2B 06 01 02 01 04 0A 00 41 02 01 AD 30 ...+.......A...0 +0320: 0D 06 08 2B 06 01 02 01 04 0B 00 41 01 00 30 0D ...+.......A..0. +0336: 06 08 2B 06 01 02 01 04 0C 00 41 01 00 30 0D 06 ..+.......A..0.. +0352: 08 2B 06 01 02 01 04 0D 00 02 01 1E 30 0D 06 08 .+..........0... +0368: 2B 06 01 02 01 04 0E 00 41 01 00 +.......A.. diff --git a/puresnmp/test/data/dummy.hex b/puresnmp/test/data/dummy.hex new file mode 100644 index 0000000..52be53a --- /dev/null +++ b/puresnmp/test/data/dummy.hex @@ -0,0 +1,9 @@ +30 81 80 02 01 01 04 06 70 75 62 6c 69 63 a2 73 0.......public.s +02 05 00 c2 71 e0 30 02 01 00 02 01 00 30 64 30 ....q.0......0d0 +62 06 08 2b 06 01 02 01 01 01 00 04 56 4c 69 6e b..+........VLin +75 78 20 64 32 34 63 66 37 66 33 36 31 33 38 20 ux d24cf7f36138 +34 2e 34 2e 30 2d 32 38 2d 67 65 6e 65 72 69 63 4.4.0-28-generic +20 23 34 37 2d 55 62 75 6e 74 75 20 53 4d 50 20 #47-Ubuntu SMP +46 72 69 20 4a 75 6e 20 32 34 20 31 30 3a 30 39 Fri Jun 24 10:09 +3a 31 33 20 55 54 43 20 32 30 31 36 20 78 38 36 :13 UTC 2016 x86 +5f 36 34 _64 diff --git a/puresnmp/test/data/getnext_response.hex b/puresnmp/test/data/getnext_response.hex new file mode 100644 index 0000000..04ebda3 --- /dev/null +++ b/puresnmp/test/data/getnext_response.hex @@ -0,0 +1,4 @@ +0000: 30 2F 02 01 01 04 06 70 75 62 6C 69 63 A2 22 02 0/.....public.". +0016: 04 7C 87 51 03 02 01 00 02 01 00 30 14 30 12 06 .|.Q.......0.0.. +0032: 0A 2B 06 01 06 03 01 01 06 01 00 02 04 15 21 95 .+............!. +0048: BE . diff --git a/puresnmp/test/test_package_root.py b/puresnmp/test/test_package_root.py index 58a9ce6..2c56d3d 100644 --- a/puresnmp/test/test_package_root.py +++ b/puresnmp/test/test_package_root.py @@ -6,26 +6,43 @@ """ -from unittest.mock import patch +from unittest.mock import patch, call import unittest -from puresnmp import get, walk, set, multiget, multiwalk, multiset +from puresnmp import ( + BulkResult, + bulkget, + bulkwalk, + get, + getnext, + multiget, + multiset, + multiwalk, + set, + table, + walk, +) from puresnmp.const import Version from puresnmp.exc import SnmpError, NoSuchOID -from puresnmp.pdu import GetRequest, VarBind +from puresnmp.pdu import GetRequest, VarBind, GetNextRequest, BulkGetRequest from puresnmp.types import Gauge -from puresnmp.x690.types import ObjectIdentifier, Integer, OctetString, Sequence +from puresnmp.x690.types import ( + Integer, + ObjectIdentifier, + OctetString, + Sequence, +) from . import readbytes -class TestApi(unittest.TestCase): +class TestGet(unittest.TestCase): def test_get_call_args(self): """ Test the call arguments of "get" """ - data = readbytes('get_sysdescr_01.hex') # any dump would do + data = readbytes('dummy.hex') # any dump would do packet = Sequence( Integer(Version.V2C), OctetString('public'), @@ -76,6 +93,9 @@ def test_get_non_existing_oid(self): with self.assertRaises(NoSuchOID): get('::1', 'private', '1.2.3') + +class TestWalk(unittest.TestCase): + def test_walk(self): response_1 = readbytes('walk_response_1.hex') response_2 = readbytes('walk_response_2.hex') @@ -118,6 +138,9 @@ def test_walk_multiple_return_binds(self): with self.assertRaisesRegexp(SnmpError, 'varbind'): next(walk('::1', 'private', '1.2.3')) + +class TestSet(unittest.TestCase): + def test_set_without_type(self): """ As we need typing information, we have to hand in an instance of @@ -145,6 +168,9 @@ def test_set_multiple_varbind(self): set('::1', 'private', '1.3.6.1.2.1.1.4.0', OctetString(b'hello@world.com')) + +class TestMultiGet(unittest.TestCase): + def test_multiget(self): data = readbytes('multiget_response.hex') expected = ['1.3.6.1.4.1.8072.3.2.10', @@ -158,6 +184,9 @@ def test_multiget(self): ]) self.assertEqual(result, expected) + +class TestMultiWalk(unittest.TestCase): + def test_multi_walk(self): response_1 = readbytes('multiwalk_response_1.hex') response_2 = readbytes('multiwalk_response_2.hex') @@ -197,7 +226,11 @@ def mocked_responses(*args, **kwargs): '1.3.6.1.2.1.2.2.1.1', '1.3.6.1.2.1.2.2.1.2' ])) - self.assertEqual(result, expected) + # TODO (advanced): should order matter in the following result? + self.assertCountEqual(result, expected) + + +class TestMultiSet(unittest.TestCase): def test_multiset(self): """ @@ -218,3 +251,231 @@ def test_multiset(self): '1.3.6.1.2.1.1.5.0': b'hello@world.com', } self.assertEqual(result, expected) + + +class TestGetNext(unittest.TestCase): + + def test_get_call_args(self): + data = readbytes('dummy.hex') # any dump would do + packet = Sequence( + Integer(Version.V2C), + OctetString('public'), + GetNextRequest(0, ObjectIdentifier(1, 2, 3)) + ) + with patch('puresnmp.send') as mck, \ + patch('puresnmp.get_request_id') as mck2: + mck2.return_value = 0 + mck.return_value = data + getnext('::1', 'public', '1.2.3') + mck.assert_called_with('::1', 161, bytes(packet)) + + def test_getnext(self): + data = readbytes('getnext_response.hex') + # TODO (beginner): The "Integer" class should not leak out! + expected = VarBind('1.3.6.1.6.3.1.1.6.1.0', Integer(354522558)) + + with patch('puresnmp.send') as mck: + mck.return_value = data + result = getnext('::1', 'private', '1.3.6.1.5') + self.assertEqual(result, expected) + + +class TestGetBulkGet(unittest.TestCase): + + def test_get_call_args(self): + data = readbytes('dummy.hex') # any dump would do + packet = Sequence( + Integer(Version.V2C), + OctetString('public'), + BulkGetRequest(0, 1, 2, + ObjectIdentifier(1, 2, 3), + ObjectIdentifier(1, 2, 4)) + ) + with patch('puresnmp.send') as mck, \ + patch('puresnmp.get_request_id') as mck2: + mck2.return_value = 0 + mck.return_value = data + bulkget('::1', 'public', + ['1.2.3'], + ['1.2.4'], + max_list_size=2) + mck.assert_called_with('::1', 161, bytes(packet)) + + + def test_bulkget(self): + data = readbytes('bulk_get_response.hex') + expected = BulkResult( + {'1.3.6.1.2.1.1.1.0': b'Linux 7e68e60fe303 4.4.0-28-generic ' + b'#47-Ubuntu SMP Fri Jun 24 10:09:13 UTC 2016 x86_64'}, + {'1.3.6.1.2.1.3.1.1.1.10.1.172.17.0.1': 10, + '1.3.6.1.2.1.3.1.1.2.10.1.172.17.0.1': b'\x02B\xe2\xc5\x8d\t', + '1.3.6.1.2.1.3.1.1.3.10.1.172.17.0.1': b'\xac\x11\x00\x01', + '1.3.6.1.2.1.4.1.0': 1, + '1.3.6.1.2.1.4.3.0': 57}) + + with patch('puresnmp.send') as mck: + mck.return_value = data + result = bulkget('::1', 'public', + ['1.3.6.1.2.1.1.1'], + ['1.3.6.1.2.1.3.1'], + max_list_size=5) + self.assertEqual(result, expected) + + +class TestGetBulkWalk(unittest.TestCase): + + def test_get_call_args(self): + data = readbytes('dummy.hex') # any dump would do + packet = Sequence( + Integer(Version.V2C), + OctetString('public'), + BulkGetRequest(0, 0, 2, ObjectIdentifier(1, 2, 3)) + ) + with patch('puresnmp.send') as mck, \ + patch('puresnmp.get_request_id') as mck2: + mck2.return_value = 0 + mck.return_value = data + + # we need to wrap this in a list to consume the generator. + list(bulkwalk('::1', 'public', + ['1.2.3'], + bulk_size=2)) + mck.assert_called_with('::1', 161, bytes(packet)) + + + @patch('puresnmp.send') + @patch('puresnmp.get_request_id') + def test_bulkwalk(self, mck_rid, mck_send): + req1 = readbytes('bulkwalk_request_1.hex') + req2 = readbytes('bulkwalk_request_2.hex') + req3 = readbytes('bulkwalk_request_3.hex') + + responses = [ + readbytes('bulkwalk_response_1.hex'), + readbytes('bulkwalk_response_2.hex'), + readbytes('bulkwalk_response_3.hex'), + ] + mck_send.side_effect = responses + + request_ids = [1001613222, 1001613223, 1001613224] + mck_rid.side_effect = request_ids + + result = list(bulkwalk('127.0.0.1', 'private', ['1.3.6.1.2.1.2.2'], + bulk_size=20)) + + self.assertEqual(mck_send.mock_calls, [ + call('127.0.0.1', 161, req1), + call('127.0.0.1', 161, req2), + call('127.0.0.1', 161, req3), + ]) + + # TODO (advanced): Type information is lost for timeticks and OIDs + expected = [ + VarBind('1.3.6.1.2.1.2.2.1.1.1', 1), + VarBind('1.3.6.1.2.1.2.2.1.1.10', 10), + VarBind('1.3.6.1.2.1.2.2.1.2.1', b"lo"), + VarBind('1.3.6.1.2.1.2.2.1.2.10', b"eth0"), + VarBind('1.3.6.1.2.1.2.2.1.3.1', 24), + VarBind('1.3.6.1.2.1.2.2.1.3.10', 6), + VarBind('1.3.6.1.2.1.2.2.1.4.1', 65536), + VarBind('1.3.6.1.2.1.2.2.1.4.10', 1500), + VarBind('1.3.6.1.2.1.2.2.1.5.1', 10000000), + VarBind('1.3.6.1.2.1.2.2.1.5.10', 4294967295), + VarBind('1.3.6.1.2.1.2.2.1.6.1', b""), + VarBind('1.3.6.1.2.1.2.2.1.6.10', b"\x02\x42\xAC\x11\x00\x02"), + VarBind('1.3.6.1.2.1.2.2.1.7.1', 1), + VarBind('1.3.6.1.2.1.2.2.1.7.10', 1), + VarBind('1.3.6.1.2.1.2.2.1.8.1', 1), + VarBind('1.3.6.1.2.1.2.2.1.8.10', 1), + VarBind('1.3.6.1.2.1.2.2.1.9.1', 0), # TODO: type info is lost + VarBind('1.3.6.1.2.1.2.2.1.9.10', 0), # TODO: type info is lost + VarBind('1.3.6.1.2.1.2.2.1.10.1', 172), + VarBind('1.3.6.1.2.1.2.2.1.10.10', 60558), + VarBind('1.3.6.1.2.1.2.2.1.11.1', 2), + VarBind('1.3.6.1.2.1.2.2.1.11.10', 564), + VarBind('1.3.6.1.2.1.2.2.1.12.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.12.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.13.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.13.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.14.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.14.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.15.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.15.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.16.1', 172), + VarBind('1.3.6.1.2.1.2.2.1.16.10', 44295), + VarBind('1.3.6.1.2.1.2.2.1.17.1', 2), + VarBind('1.3.6.1.2.1.2.2.1.17.10', 442), + VarBind('1.3.6.1.2.1.2.2.1.18.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.18.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.19.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.19.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.20.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.20.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.21.1', 0), + VarBind('1.3.6.1.2.1.2.2.1.21.10', 0), + VarBind('1.3.6.1.2.1.2.2.1.22.1', '0.0'), # TODO: type info is lost + VarBind('1.3.6.1.2.1.2.2.1.22.10', '0.0'), # TODO: type info is lost + ] + + # TODO: Expected types per OID: + # 1.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1 + # 1.3.6.1.2.1.2.2.1.1.10 = INTEGER: 10 + # 1.3.6.1.2.1.2.2.1.2.1 = STRING: "lo" + # 1.3.6.1.2.1.2.2.1.2.10 = STRING: "eth0" + # 1.3.6.1.2.1.2.2.1.3.1 = INTEGER: 24 + # 1.3.6.1.2.1.2.2.1.3.10 = INTEGER: 6 + # 1.3.6.1.2.1.2.2.1.4.1 = INTEGER: 65536 + # 1.3.6.1.2.1.2.2.1.4.10 = INTEGER: 1500 + # 1.3.6.1.2.1.2.2.1.5.1 = Gauge32: 10000000 + # 1.3.6.1.2.1.2.2.1.5.10 = Gauge32: 4294967295 + # 1.3.6.1.2.1.2.2.1.6.1 = "" + # 1.3.6.1.2.1.2.2.1.6.10 = Hex-STRING: 02 42 AC 11 00 02 + # 1.3.6.1.2.1.2.2.1.7.1 = INTEGER: 1 + # 1.3.6.1.2.1.2.2.1.7.10 = INTEGER: 1 + # 1.3.6.1.2.1.2.2.1.8.1 = INTEGER: 1 + # 1.3.6.1.2.1.2.2.1.8.10 = INTEGER: 1 + # 1.3.6.1.2.1.2.2.1.9.1 = Timeticks: (0) 0:00:00.00 + # 1.3.6.1.2.1.2.2.1.9.10 = Timeticks: (0) 0:00:00.00 + # 1.3.6.1.2.1.2.2.1.10.1 = Counter32: 172 + # 1.3.6.1.2.1.2.2.1.10.10 = Counter32: 60558 + + # 1.3.6.1.2.1.2.2.1.11.1 = Counter32: 2 + # 1.3.6.1.2.1.2.2.1.11.10 = Counter32: 564 + # 1.3.6.1.2.1.2.2.1.12.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.12.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.13.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.13.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.14.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.14.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.15.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.15.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.16.1 = Counter32: 172 + # 1.3.6.1.2.1.2.2.1.16.10 = Counter32: 44295 + # 1.3.6.1.2.1.2.2.1.17.1 = Counter32: 2 + # 1.3.6.1.2.1.2.2.1.17.10 = Counter32: 442 + # 1.3.6.1.2.1.2.2.1.18.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.18.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.19.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.19.10 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.20.1 = Counter32: 0 + # 1.3.6.1.2.1.2.2.1.20.10 = Counter32: 0 + + # 1.3.6.1.2.1.2.2.1.21.1 = Gauge32: 0 + # 1.3.6.1.2.1.2.2.1.21.10 = Gauge32: 0 + # 1.3.6.1.2.1.2.2.1.22.1 = OID: ccitt.0 + # 1.3.6.1.2.1.2.2.1.22.10 = OID: ccitt.0 + self.assertEqual(result, expected) + + +class TestGetTable(unittest.TestCase): + + @patch('puresnmp.walk') + @patch('puresnmp.tablify') + @patch('puresnmp.get_request_id') + def test_table(self, mck_rid, mck_tablify, mck_walk): + mck_rid.return_value = 0 + tmp = object() # dummy return value + mck_walk.return_value = tmp + table('::1', 'public', '1.2.3.4', port=161, num_base_nodes=2) + mck_walk.assert_called_with('::1', 'public', '1.2.3.4', port=161) + mck_tablify.assert_called_with(tmp, num_base_nodes=2) diff --git a/puresnmp/test/test_pdus.py b/puresnmp/test/test_pdus.py index 406584a..e3087c8 100644 --- a/puresnmp/test/test_pdus.py +++ b/puresnmp/test/test_pdus.py @@ -1,11 +1,13 @@ from ..exc import SnmpError from ..x690.types import ( Integer, + Null, ObjectIdentifier, OctetString, Sequence, ) from ..pdu import ( + BulkGetRequest, GetNextRequest, GetRequest, GetResponse, @@ -190,3 +192,29 @@ def test_request(self): ) result = bytes(packet) self.assertBytesEqual(result, expected) + + +class TestBulkGet(ByteTester): + """ + BulkGet also receives a default "get" response, so there's no need to test + this in this TestCase. + """ + + def test_request(self): + expected = readbytes('bulk_get_request.hex') + + request = BulkGetRequest( + 437387882, + 0, # non-repeaters + 5, # max-repeaters + ObjectIdentifier.from_string('1.3.6.1.2.1.2.2.0'), + ObjectIdentifier.from_string('1.3.6.1.2.1.2.3.0') + ) + packet = Sequence( + Integer(Version.V2C), + OctetString('public'), + request + ) + + result = bytes(packet) + self.assertBytesEqual(result, expected) diff --git a/puresnmp/test/x690/test_types.py b/puresnmp/test/x690/test_types.py index 24a75d1..2df5cda 100644 --- a/puresnmp/test/x690/test_types.py +++ b/puresnmp/test/x690/test_types.py @@ -196,6 +196,16 @@ def test_hash(self): expected = hash(ObjectIdentifier(1, 2, 3)) self.assertEqual(result, expected) + def test_non_containment_f(self): + """ + This case showed up during development of bulk operations. Throwing it + into the unit tests to ensure proper containment checks. + """ + a = ObjectIdentifier(1, 3, 6, 1, 2, 1, 2, 2, 1, 22) + b = ObjectIdentifier(1, 3, 6, 1, 2, 1, 2, 2, 1, 10, 38) + self.assertNotIn(a, b, '%s should not be in %s' % (a, b)) + self.assertNotIn(b, a, '%s should not be in %s' % (b, a)) + class TestInteger(ByteTester): diff --git a/puresnmp/x690/types.py b/puresnmp/x690/types.py index 8851520..0d6c392 100644 --- a/puresnmp/x690/types.py +++ b/puresnmp/x690/types.py @@ -546,7 +546,10 @@ def __contains__(self, other): if all([x is None for x in unzipped_b]): return True - return unzipped_a < unzipped_b + if len(tail) > 1: + return False + else: + return unzipped_a < unzipped_b def __hash__(self): return hash(self.identifiers)