diff --git a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py index 0b2bbc0480fb6d763e8b415778009f5aca7c40f1..28c0e2a64200d27cb4d5a8f3b20275507b1abdf6 100644 --- a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py +++ b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py @@ -20,6 +20,7 @@ snmp_to_numpy_dict = { hlapi.IpAddress: str, } + class SNMP_client(CommClient): """ messages to keep a check on the connection. On connection failure, reconnects once. @@ -62,35 +63,8 @@ class SNMP_client(CommClient): index (optional) the index if the value thats being read from is a table. """ - # return values start as None because we have a way too complicated interface - oids = None - mib = None - name = None - idx = None - - # check if the 'oids' key is used and not the 'mib' and 'name' keys - - if 'oids' in annotation and 'mib' not in annotation and 'name' not in annotation: - uses_oid = True - oids = annotation["oids"] - - # checks to make sure this isn't present - if 'index' in annotation: - raise ValueError(f"SNMP attribute annotation doesn't support oid type declarations with an index present.") - - - # check if the 'oids' key is NOT used but instead the 'mib' and 'name' keys - elif 'oids' not in annotation and 'mib' in annotation and 'name' in annotation: - mib = annotation["mib"] - name = annotation["name"] - - # SNMP has tables that require an index number to access them. regular non-table variable have an index of 0 - idx = annotation.get('index', 0) - - else: - raise ValueError(f"SNMP attribute annotation requires a dict argument with either a 'oids' key or both a 'name' and 'mib' key. Not both. Instead got: {annotation}") - - return oids, mib, name, idx + wrapper = annotation_wrapper(annotation) + return wrapper def setup_value_conversion(self, attribute): """ @@ -113,11 +87,11 @@ class SNMP_client(CommClient): """ # process the annotation - oids, mib, name, idx = self._setup_annotation(annotation) + wrapper = self._setup_annotation(annotation) # get all the necessary data to set up the read/write functions from the attribute_wrapper dim_x, dim_y, dtype = self.setup_value_conversion(attribute) - snmp_attr = snmp_attribute(self, oids, mib, name, idx, dtype, dim_x, dim_y) + snmp_attr = snmp_attribute(self, wrapper, dtype, dim_x, dim_y) # return the read/write functions def read_function(): @@ -129,45 +103,125 @@ class SNMP_client(CommClient): return read_function, write_function -class snmp_attribute: +class annotation_wrapper: + def __init__(self, annotation): + """ + The SNMP client uses a dict and takes the following keys: - def __init__(self, client : SNMP_client, oids, mib, name, idx, dtype, dim_x, dim_y): + either + oids: Required. An oid string of the object + or + mib: the mib name + name: name of the value to read + index (optional) the index if the value thats being read from is a table. + """ - self.client = client - self.dtype = dtype - self.dim_x = dim_x - self.dim_y = dim_y - self.is_scalar = (self.dim_x + self.dim_y) == 1 + # values start as None because we have a way too complicated interface + self.oids = None + self.mib = None + self.name = None + self.idx = None + + # check if the 'oids' key is used and not the 'mib' and 'name' keys + + if 'oids' in annotation and 'mib' not in annotation and 'name' not in annotation: + self.oids = annotation["oids"] + + # checks to make sure this isn't present + if 'index' in annotation: + raise ValueError(f"SNMP attribute annotation doesn't support oid type declarations with an index present.") + + + # check if the 'oids' key is NOT used but instead the 'mib' and 'name' keys + elif 'oids' not in annotation and 'mib' in annotation and 'name' in annotation: + self.mib = annotation["mib"] + self.name = annotation["name"] + + # SNMP has tables that require an index number to access them. regular non-table variable have an index of 0 + self.idx = annotation.get('index', 0) + + else: + raise ValueError( + f"SNMP attribute annotation requires a dict argument with either a 'oids' key or both a 'name' and 'mib' key. Not both. Instead got: {annotation}") + + def create_objID(self, x, y): + is_scalar = (x + y) == 1 # if oids are used - if oids is not None: + if self.oids is not None: # get a list of str of the oids - oids = self.get_oids(dim_x, dim_y, oids) + self.oids = self._get_oids(x, y, self.oids) # turn the list of oids in to a tuple of pysnmp object identities. These are used for the - objID = tuple(hlapi.ObjectIdentity(oids[i]) for i in range(len(oids))) + objID = tuple(hlapi.ObjectIdentity(self.oids[i]) for i in range(len(self.oids))) # if mib + name is used else: # only scalars can be used at the present time. - if not self.is_scalar: - #tuple(hlapi.ObjectIdentity(mib, name, idx) for i in range(len(oids))) + if not is_scalar: + # tuple(hlapi.ObjectIdentity(mib, name, idx) for i in range(len(oids))) - raise ValueError(f"MIB + name type attributes can only be scalars, got dimensions of: ({self.dim_y}, {self.dim_x})") + raise ValueError(f"MIB + name type attributes can only be scalars, got dimensions of: ({x}, {y})") else: - objID = hlapi.ObjectIdentity(mib, name, idx) + objID = hlapi.ObjectIdentity(self.mib, self.name, self.idx) + + return objID + + def _get_oids(self, x, y, in_oid): - self.objID = objID + if x == 0: + x = 1 + if y == 0: + y = 1 + + is_scalar = (x * y) == 1 + nof_oids = x * y + + # if scalar + if is_scalar: + if type(in_oid) is str: + # for ease of handling put single oid in a 1 element list + in_oid = [in_oid] + + return in_oid + + else: + # if we got a single str oid, make a list of sequential oids + if type(in_oid) is str: + return ["{}.{}".format(in_oid, i + 1) for i in range(nof_oids)] + # if its an already expanded list of all oids + elif type(in_oid) is list and len(in_oid) == nof_oids: + # already is an list and of the right length + return in_oid + # if its a list of attributes with the wrong length. + else: + raise ValueError( + "SNMP oids need to either be a single value or an array the size of the attribute dimensions. got: {} expected: {}x{}={}".format( + len(in_oid), x, y, x * y)) + + +class snmp_attribute: + + def __init__(self, client : SNMP_client, wrapper, dtype, dim_x, dim_y): + + self.client = client + self.wrapper = wrapper + self.dtype = dtype + self.dim_x = dim_x + self.dim_y = dim_y + self.is_scalar = (self.dim_x + self.dim_y) == 1 + + self.objID = self.wrapper.create_objID(self.dim_x, self.dim_y) def next_wrap(self, cmd): """ This function exists to allow the next(cmd) call to be mocked for unit testing. As the """ - return next(cmd) def read_function(self): + # must be recreated for each read it seems self.objs = tuple(hlapi.ObjectType(i) for i in self.objID) # get the thingy to get the values @@ -193,37 +247,6 @@ class snmp_attribute: set_cmd = hlapi.setCmd(self.client.engine, self.client.community, self.client.trasport, self.client.ctx_data, *write_obj) errorIndication, errorStatus, errorIndex, *varBinds = self.next_wrap(set_cmd) - - def get_oids(self, x, y, in_oid): - - if x == 0: - x = 1 - if y == 0: - y = 1 - - nof_oids = x * y - - # if scalar - if self.is_scalar: - if type(in_oid) is str: - # for ease of handling put single oid in a 1 element list - in_oid = [in_oid] - - return in_oid - - else: - # if we got a single str oid, make a list of sequential oids - if type(in_oid) is str: - return ["{}.{}".format(in_oid, i + 1) for i in range(nof_oids)] - # if its an already expanded list of all oids - elif type(in_oid) is list and len(in_oid) == nof_oids: - # already is an list and of the right length - return in_oid - # if its a list of attributes with the wrong length. - else: - raise ValueError("SNMP oids need to either be a single value or an array the size of the attribute dimensions. got: {} expected: {}x{}={}".format(len(in_oid),x,y,x*y)) - - def convert(self, varBinds): """ get all the values in a list, make sure to convert specific types that dont want to play nicely diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py index 2bb4cd60f28bdd1be32924f85dea9b64dd49f5b2..05da065b95b34387da517a66083dd6c672994643 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py @@ -5,7 +5,7 @@ from unittest import mock from tangostationcontrol.test import base -from tangostationcontrol.clients.snmp_client import SNMP_client, snmp_attribute +from tangostationcontrol.clients.snmp_client import SNMP_client, snmp_attribute, annotation_wrapper snmp_to_numpy_dict = { hlapi.Integer32: numpy.int64, @@ -81,60 +81,61 @@ def val_check(snmp_type : type, dims : tuple): class TestSNMP(base.TestCase): - @mock.patch('pysnmp.hlapi.ObjectIdentity') - @mock.patch('pysnmp.hlapi.ObjectType') - @mock.patch('tangostationcontrol.clients.snmp_client.snmp_attribute.next_wrap') - def test_snmp_obj_get(self, m_next, m_obj_T, m_obj_i): + + def test_annotation_success(self): """ - Attempts to read a fake SNMP variable and checks whether it got what it expected + unit test for the processing of annotation. Has 2 lists. 1 with things that should succeed and 1 with things that should fail. """ - for j in dim_list: - for i in snmp_to_numpy_dict: - m_next.return_value = (None, None, None, get_return_val(i, dim_list[j])) - - m_client = mock.Mock() + test_name = "test_name" + test_mib = "test_mib" - a = snmp_attribute(client=m_client, oids="1.3.6.1.2.1.2.2.1.2.31", mib=None, name=None, idx=None, dtype=snmp_to_numpy_dict[i], dim_x=dim_list[j][0], dim_y=dim_list[j][1]) - val = a.read_function() + client = SNMP_client(community='public', host='localhost', timeout=10, fault_func=None, try_interval=2) - checkval = val_check(i, dim_list[j]) - self.assertEqual(checkval, val, f"Expected: {checkval}, got: {val}") + test_list = [ + # test name nad MIB type annotation + {"mib": "SNMPv2-MIB", "name": "sysDescr"}, - @mock.patch('pysnmp.hlapi.ObjectIdentity') - @mock.patch('pysnmp.hlapi.setCmd') - @mock.patch('tangostationcontrol.clients.snmp_client.snmp_attribute.next_wrap') - def test_snmp_obj_set(self, m_next, m_nextCmd, m_obj_i): - """ - Attempts to write a value to an SNMP server, but instead intercepts it and compared whether the values is as expected. - """ + # test name nad MIB type annotation with index + {"mib": "RFC1213-MIB", "name": "ipAdEntAddr", "index": (127, 0, 0, 1)}, + {"mib": "random-MIB", "name": "aName", "index": 2}, - for j in dim_list: - for i in snmp_to_numpy_dict: - m_next.return_value = (None, None, None, get_return_val(i, dim_list[j])) + #oid + {"oids": "1.3.6.1.2.1.2.2.1.2.31"} + ] - m_client = mock.Mock() - set_val = val_check(i, dim_list[j]) + for i in test_list: + wrapper = client._setup_annotation(annotation=i) - a = snmp_attribute(client=m_client, oids="1.3.6.1.2.1.2.2.1.2.31", mib=None, name=None, idx=None, dtype=snmp_to_numpy_dict[i], dim_x=dim_list[j][0], - dim_y=dim_list[j][1]) + if wrapper.oids is not None: + self.assertEqual(wrapper.oids, i["oids"]) - res_lst = [] - def test(*value): - res_lst.append(value[1]) - return None, None, None, get_return_val(i, dim_list[j]) + else: + self.assertEqual(wrapper.mib, i["mib"], f"expected mib with: {i['mib']}, got: {wrapper.idx} from: {i}") + self.assertEqual(wrapper.name, i["name"], f"expected name with: {i['name']}, got: {wrapper.idx} from: {i}") + self.assertEqual(wrapper.idx, i.get('index', 0), f"expected idx with: {i.get('index', 0)}, got: {wrapper.idx} from: {i}") - hlapi.ObjectType = test - a.write_function(set_val) + def test_annotation_fail(self): + """ + unit test for the processing of annotation. Has 2 lists. 1 with things that should succeed and 1 with things that should fail. + """ - if len(res_lst) == 1: - res_lst = res_lst[0] + client = SNMP_client(community='public', host='localhost', timeout=10, fault_func=None, try_interval=2) - checkval = val_check(i, dim_list[j]) - self.assertEqual(checkval, res_lst, f"Expected: {checkval}, got: {res_lst}") + fail_list = [ + # OIDS cant use the index + {"oids": "1.3.6.1.2.1.2.2.1.2.31", "index": 2}, + # mixed annotation is not allowed + {"oids": "1.3.6.1.2.1.2.2.1.2.31", "name": "thisShouldFail"}, + # no 'name' + {"mib": "random-MIB", "index": 2}, + ] + for i in fail_list: + with self.assertRaises(ValueError): + client._setup_annotation(annotation=i) def test_oids_scalar(self): @@ -145,12 +146,10 @@ class TestSNMP(base.TestCase): m_client = mock.Mock() # we just need the object to call another function - a = snmp_attribute(client=m_client, oids="Not None", mib=None, name=None, idx=None, dtype=str, dim_x=x, dim_y=y) - + wrapper = annotation_wrapper(annotation = {"oids": "Not None lol"}) # scalar scalar_expected = [test_oid] - scalar_dims = (x, y) - ret_oids = a.get_oids(scalar_dims[0], scalar_dims[1], test_oid) + ret_oids = wrapper._get_oids(x, y, test_oid) self.assertEqual(ret_oids, scalar_expected, f"Expected: {scalar_expected}, got: {ret_oids}") def test_oids_spectrum(self): @@ -167,66 +166,68 @@ class TestSNMP(base.TestCase): m_client = mock.Mock() # we just need the object to call another function - a = snmp_attribute(client=m_client, oids="Not None", mib=None, name=None, idx=None, dtype=str, dim_x=x, dim_y=y) + wrapper = annotation_wrapper(annotation={"oids": "Not None lol"}) # spectrum spectrum_expected = [test_oid + ".1", test_oid + ".2", test_oid + ".3", test_oid + ".4"] - spectrum_dims = (x, y) - ret_oids = a.get_oids(spectrum_dims[0], spectrum_dims[1], test_oid) + ret_oids = wrapper._get_oids(x, y, test_oid) self.assertListEqual(ret_oids, spectrum_expected, f"Expected: {spectrum_expected}, got: {ret_oids}") - - def test_annotation_success(self): + @mock.patch('pysnmp.hlapi.ObjectIdentity') + @mock.patch('pysnmp.hlapi.ObjectType') + @mock.patch('tangostationcontrol.clients.snmp_client.snmp_attribute.next_wrap') + def test_snmp_obj_get(self, m_next, m_obj_T, m_obj_i): """ - unit test for the processing of annotation. Has 2 lists. 1 with things that should succeed and 1 with things that should fail. + Attempts to read a fake SNMP variable and checks whether it got what it expected """ - test_name = "test_name" - test_mib = "test_mib" + for j in dim_list: + for i in snmp_to_numpy_dict: + m_next.return_value = (None, None, None, get_return_val(i, dim_list[j])) - client = SNMP_client(community='public', host='localhost', timeout=10, fault_func=None, try_interval=2) + m_client = mock.Mock() - test_list = [ - # test name nad MIB type annotation - {"mib": "SNMPv2-MIB", "name": "sysDescr"}, + wrapper = annotation_wrapper(annotation={"oids": "1.3.6.1.2.1.2.2.1.2.31"}) + a = snmp_attribute(client=m_client, wrapper=wrapper, dtype=snmp_to_numpy_dict[i], dim_x=dim_list[j][0], dim_y=dim_list[j][1]) - # test name nad MIB type annotation with index - {"mib": "RFC1213-MIB", "name": "ipAdEntAddr", "index": (127, 0, 0, 1)}, - {"mib": "random-MIB", "name": "aName", "index": 2}, + val = a.read_function() - #oid - {"oids": "1.3.6.1.2.1.2.2.1.2.31"} - ] + checkval = val_check(i, dim_list[j]) + self.assertEqual(checkval, val, f"Expected: {checkval}, got: {val}") + @mock.patch('pysnmp.hlapi.ObjectIdentity') + @mock.patch('pysnmp.hlapi.setCmd') + @mock.patch('tangostationcontrol.clients.snmp_client.snmp_attribute.next_wrap') + def test_snmp_obj_set(self, m_next, m_nextCmd, m_obj_i): + """ + Attempts to write a value to an SNMP server, but instead intercepts it and compared whether the values is as expected. + """ - for i in test_list: - oids, mib, name, idx = client._setup_annotation(annotation=i) + for j in dim_list: + for i in snmp_to_numpy_dict: + m_next.return_value = (None, None, None, get_return_val(i, dim_list[j])) - if oids is not None: - self.assertEqual(oids, i["oids"]) + m_client = mock.Mock() + set_val = val_check(i, dim_list[j]) - else: - self.assertEqual(mib, i["mib"], f"expected mib with: {i['mib']}, got: {idx} from: {i}") - self.assertEqual(name, i["name"], f"expected name with: {i['name']}, got: {idx} from: {i}") - self.assertEqual(idx, i.get('index', 0), f"expected idx with: {i.get('index', 0)}, got: {idx} from: {i}") + wrapper = annotation_wrapper(annotation={"oids": "1.3.6.1.2.1.2.2.1.2.31"}) + a = snmp_attribute(client=m_client, wrapper=wrapper, dtype=snmp_to_numpy_dict[i], dim_x=dim_list[j][0], dim_y=dim_list[j][1]) + res_lst = [] + def test(*value): + res_lst.append(value[1]) + return None, None, None, get_return_val(i, dim_list[j]) + + hlapi.ObjectType = test + + a.write_function(set_val) + + if len(res_lst) == 1: + res_lst = res_lst[0] + + checkval = val_check(i, dim_list[j]) + self.assertEqual(checkval, res_lst, f"Expected: {checkval}, got: {res_lst}") - def test_annotation_fail(self): - """ - unit test for the processing of annotation. Has 2 lists. 1 with things that should succeed and 1 with things that should fail. - """ - client = SNMP_client(community='public', host='localhost', timeout=10, fault_func=None, try_interval=2) - fail_list = [ - # OIDS cant use the index - {"oids": "1.3.6.1.2.1.2.2.1.2.31", "index": 2}, - # mixed annotation is not allowed - {"oids": "1.3.6.1.2.1.2.2.1.2.31", "name": "thisShouldFail"}, - # no 'name' - {"mib": "random-MIB", "index": 2}, - ] - for i in fail_list: - with self.assertRaises(ValueError): - client._setup_annotation(annotation=i)