diff --git a/tangostationcontrol/tangostationcontrol/examples/snmp/__init__.py b/tangostationcontrol/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/examples/snmp/__init__.py rename to tangostationcontrol/__init__.py diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt index 0cb186f10ceb7b41693b948a4abd81b17c61c053..62cf05034ea7b40c479f6b75eb4c8d2641f2dcf9 100644 --- a/tangostationcontrol/requirements.txt +++ b/tangostationcontrol/requirements.txt @@ -6,7 +6,7 @@ asyncua >= 0.9.90 # LGPLv3 PyMySQL[rsa] >= 1.0.2 # MIT psycopg2-binary >= 2.9.2 # LGPL sqlalchemy >= 1.4.26 # MIT -snmp >= 0.1.7 # GPL3 +pysnmp >= 0.1.7 # BSD h5py >= 3.1.0 # BSD psutil >= 5.8.0 # BSD docker >= 5.0.3 # Apache 2 diff --git a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py new file mode 100644 index 0000000000000000000000000000000000000000..7a7f45808cdc2d160cb9db3356d3a0e9beda4be0 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py @@ -0,0 +1,282 @@ + +from tangostationcontrol.clients.comms_client import CommClient + +from pysnmp import hlapi + +import numpy +import logging + +logger = logging.getLogger() + +__all__ = ["SNMP_client"] + +snmp_to_numpy_dict = { + hlapi.Integer32: numpy.int64, + hlapi.TimeTicks: numpy.int64, + str: str, + hlapi.ObjectIdentity: str, + hlapi.Counter32: numpy.int64, + hlapi.Gauge32: numpy.int64, + hlapi.IpAddress: str, +} + + +class SNMP_client(CommClient): + """ + messages to keep a check on the connection. On connection failure, reconnects once. + """ + + def start(self): + super().start() + + def __init__(self, community, host, timeout, fault_func, try_interval=2, port=161): + """ + Create the SNMP engine + """ + super().__init__(fault_func, try_interval) + + logger.debug(f"setting up SNMP engine with host: {host} and community: {community}") + self.port = port + + self.engine = hlapi.SnmpEngine() + self.community = hlapi.CommunityData(community) + self.transport = hlapi.UdpTransportTarget((host, port)) + + # context data sets the version used. Default SNMPv2 + self.ctx_data = hlapi.ContextData() + + # only sets up the engine, doesn't connect + self.connected = True + + + def _setup_annotation(self, annotation): + """ + parses the annotation this attribute received for its initialisation. + """ + + wrapper = annotation_wrapper(annotation) + return wrapper + + def setup_value_conversion(self, attribute): + """ + gives the client access to the attribute_wrapper object in order to access all data it could potentially need. + """ + + dim_x = attribute.dim_x + dim_y = attribute.dim_y + dtype = attribute.numpy_type + + return dim_x, dim_y, dtype + + def setup_attribute(self, annotation, attribute): + """ + MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions + + Gets called from inside the attribute wrapper. It is provided with the attribute_warpper itself + and the annotation provided when the attribute_wrapper was declared. + These parameters can be used to configure a valid read and write function as return values. + """ + + # process the 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, wrapper, dtype, dim_x, dim_y) + + # return the read/write functions + def read_function(): + return snmp_attr.read_function() + + def write_function(value): + snmp_attr.write_function(value) + + return read_function, write_function + + +class annotation_wrapper: + def __init__(self, annotation): + """ + The SNMP client uses a dict and takes the following keys: + + 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. + """ + + # 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 self.oids is not None: + # get a list of str of the 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(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 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: ({x}, {y})") + else: + objID = hlapi.ObjectIdentity(self.mib, self.name, self.idx) + + return objID + + def _get_oids(self, x, y, in_oid): + """ + This function expands oids depending on dimensionality. + if its a scalar its left alone, but if its an array it creates a list of sequential oids if not already provided + + scalar "1.1.1.1" -> stays the same + spectrum: "1.1.1.1" -> ["1.1.1.1.1", "1.1.1.1.2, ..."] + """ + + 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: + 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): + """ + Read function we give to the attribute wrapper + """ + + # 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 + get_cmd = hlapi.getCmd(self.client.engine, self.client.community, self.client.trasport, self.client.ctx_data, *self.objs) + + # dont ask me why 'next' is used to get all of the values + errorIndication, errorStatus, errorIndex, *varBinds = self.next_wrap(get_cmd) + + # get all the values in a list converted to the correct type + val_lst = self.convert(varBinds) + + # return the list of values + return val_lst + + def write_function(self, value): + """ + Write function we give to the attribute wrapper + """ + + if self.is_scalar: + write_obj = tuple(hlapi.ObjectType(self.objID[0], value), ) + + else: + write_obj = tuple(hlapi.ObjectType(self.objID[i], value[i]) for i in range(len(self.objID))) + + 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 convert(self, varBinds): + """ + get all the values in a list, make sure to convert specific types that dont want to play nicely + """ + + vals = [] + if not self.is_scalar: + #just the first element of this single element list + varBinds = varBinds[0] + + for varBind in varBinds: + # class 'DisplayString' doesnt want to play along for whatever reason + if "DisplayString" in str(type(varBind[1])): + vals.append(varBind[1].prettyPrint()) + elif type(varBind[1]) == hlapi.IpAddress: + # IpAddress values get printed as their raw value but in hex (7F 20 20 01 for 127.0.0.1 for example) + vals.append(varBind[1].prettyPrint()) + else: + # convert from the funky pysnmp types to numpy types and then append + vals.append(snmp_to_numpy_dict[type(varBind[1])](varBind[1])) + + if self.is_scalar: + vals = vals[0] + + return vals + + diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py index d26c21dfe51438d3629ceaae5be1026e3a7e8d51..dd8bd1c2a7698d2c2481dbb52d60e99541f7e43b 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apsct.py +++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py @@ -84,16 +84,16 @@ class APSCT(opcua_device): # ---------- # Summarising Attributes # ---------- - APSCT_error_R = attribute(dtype=bool) + APSCT_error_R = attribute(dtype=bool, fisallowed="is_attribute_wrapper_allowed") def read_APSCT_error_R(self): - return ((self.proxy.APSCTTR_I2C_error_R > 0) + return ((self.read_attribute("APSCTTR_I2C_error_R") > 0) or self.alarm_val("APSCT_PCB_ID_R") - or (not self.proxy.APSCT_INPUT_10MHz_good_R) - or (not self.proxy.APSCT_INPUT_PPS_good_R and not self.proxy.APSCT_PPS_ignore_R) - or (not self.proxy.APSCT_PLL_160MHz_locked_R and not self.proxy.APSCT_PLL_200MHz_locked_R) - or (self.proxy.APSCT_PLL_200MHz_locked_R and self.proxy.APSCT_PLL_200MHz_error_R) - or (self.proxy.APSCT_PLL_160MHz_locked_R and self.proxy.APSCT_PLL_160MHz_error_R) + or (not self.read_attribute("APSCT_INPUT_10MHz_good_R")) + or (not self.read_attribute("APSCT_INPUT_PPS_good_R") and not self.read_attribute("APSCT_PPS_ignore_R")) + or (not self.read_attribute("APSCT_PLL_160MHz_locked_R") and not self.read_attribute("APSCT_PLL_200MHz_locked_R")) + or (self.read_attribute("APSCT_PLL_200MHz_locked_R") and self.read_attribute("APSCT_PLL_200MHz_error_R")) + or (self.read_attribute("APSCT_PLL_160MHz_locked_R") and self.read_attribute("APSCT_PLL_160MHz_error_R")) ) APSCT_TEMP_error_R = attribute(dtype=bool) @@ -108,9 +108,9 @@ class APSCT(opcua_device): or self.alarm_val("APSCT_PWR_CLKDIST2_3V3_R") or self.alarm_val("APSCT_PWR_CTRL_3V3_R") or self.alarm_val("APSCT_PWR_INPUT_3V3_R") - or (self.proxy.APSCT_PWR_PLL_160MHz_on_R and self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R")) - or (self.proxy.APSCT_PWR_PLL_200MHz_on_R and self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R")) - or (not self.proxy.APSCT_PWR_on_R) + or (self.read_attribute("APSCT_PWR_PLL_160MHz_on_R") and self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R")) + or (self.read_attribute("APSCT_PWR_PLL_200MHz_on_R") and self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R")) + or (not self.read_attribute("APSCT_PWR_on_R")) ) # -------- @@ -126,8 +126,8 @@ class APSCT(opcua_device): self.APSCT_200MHz_on() self.wait_attribute("APSCTTR_translator_busy_R", False, self.APSCT_On_Off_timeout) - if not self.proxy.APSCT_PLL_200MHz_locked_R: - if self.proxy.APSCTTR_I2C_error_R: + if not self.read_attribute("APSCT_PLL_200MHz_locked_R"): + if self.read_attribute("APSCTTR_I2C_error_R"): raise Exception("I2C is not working. Maybe power cycle subrack to restart CLK board and translator?") else: raise Exception("200MHz signal is not locked. The subrack probably do not receive clock input or the CLK PCB is broken?") diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py index 6be213696c16efc69ca8f20319efc03cfc0f4fe7..b61b9b0e248b16953a08bad359bf47ec877b0b3d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apspu.py +++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py @@ -66,15 +66,15 @@ class APSPU(opcua_device): APSPU_error_R = attribute(dtype=bool) def read_APSPU_error_R(self): - return ((self.proxy.APSPUTR_I2C_error_R > 0) + return ((self.read_attribute("APSPUTR_I2C_error_R") > 0) or self.alarm_val("APSPU_PCB_ID_R") or self.alarm_val("APSPU_FAN1_RPM_R") or self.alarm_val("APSPU_FAN2_RPM_R") or self.alarm_val("APSPU_FAN3_RPM_R")) - APSPU_IOUT_error_R = attribute(dtype=bool) - APSPU_TEMP_error_R = attribute(dtype=bool) - APSPU_VOUT_error_R = attribute(dtype=bool) + APSPU_IOUT_error_R = attribute(dtype=bool, fisallowed="is_attribute_wrapper_allowed") + APSPU_TEMP_error_R = attribute(dtype=bool, fisallowed="is_attribute_wrapper_allowed") + APSPU_VOUT_error_R = attribute(dtype=bool, fisallowed="is_attribute_wrapper_allowed") def read_APSPU_IOUT_error_R(self): return ( self.alarm_val("APSPU_LBA_IOUT_R") diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py index c4bc5a8b2813105679b82af89285532611cbd9e8..740d45626ae02dcb62b1ff2a00d5b30b31be91ab 100644 --- a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py @@ -374,6 +374,19 @@ class lofar_device(Device, metaclass=DeviceMeta): """ Override this method to initialise any hardware after configuring it. """ pass + def read_attribute(self, attr_name): + """ Read the value of a certain attribute (directly from the hardware). """ + + # obtain the class information of this attribute, effectively equal + # to getattr(self, attr_name), but this also makes sure we actually + # address an attribute. + class_attribute = self.get_device_attr().get_attr_by_name(attr_name) + + # obtain the low-level wrapper for the read function + read_wrapper = getattr(self, f"__read_{attr_name}_wrapper__") + + # obtain the actual value + return read_wrapper(class_attribute) def wait_attribute(self, attr_name, value, timeout=10, pollperiod=0.2): """ Wait until the given attribute obtains the given value. @@ -394,7 +407,7 @@ class lofar_device(Device, metaclass=DeviceMeta): # Poll every half a second for _ in range(math.ceil(timeout/pollperiod)): - if is_correct(getattr(self.proxy, attr_name)): + if is_correct(self.read_attribute(attr_name)): return time.sleep(pollperiod) @@ -412,7 +425,7 @@ class lofar_device(Device, metaclass=DeviceMeta): is_scalar = attr_config.data_format == AttrDataFormat.SCALAR # fetch attribute value as an array - value = self.proxy.read_attribute(attr_name).value + value = self.read_attribute(attr_name) if is_scalar: value = numpy.array(value) # this stays a scalar in numpy diff --git a/tangostationcontrol/tangostationcontrol/devices/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py index eaf0f189b53ca3f64606d196298f6c9dc27708a1..787a51df3e62ed35071cd97e66ffc1851819f74a 100644 --- a/tangostationcontrol/tangostationcontrol/devices/recv.py +++ b/tangostationcontrol/tangostationcontrol/devices/recv.py @@ -255,20 +255,20 @@ class RECV(opcua_device): RCU_LED_colour_R = attribute(dtype=(numpy.uint32,), max_dim_x=32) def read_RCU_LED_colour_R(self): - return (2 * self.proxy.RCU_LED_green_on_R + 4 * self.proxy.RCU_LED_red_on_R).astype(numpy.uint32) + return (2 * self.read_attribute("RCU_LED_green_on_R") + 4 * self.read_attribute("RCU_LED_red_on_R")).astype(numpy.uint32) - RCU_error_R = attribute(dtype=(bool,), max_dim_x=32) - ANT_error_R = attribute(dtype=((bool,),), max_dim_x=3, max_dim_y=32) + RCU_error_R = attribute(dtype=(bool,), max_dim_x=32, fisallowed="is_attribute_wrapper_allowed") + ANT_error_R = attribute(dtype=((bool,),), max_dim_x=3, max_dim_y=32, fisallowed="is_attribute_wrapper_allowed") def read_RCU_error_R(self): - return self.proxy.RCU_mask_RW & ( - (self.proxy.RECVTR_I2C_error_R > 0) + return self.read_attribute("RCU_mask_RW") & ( + (self.read_attribute("RECVTR_I2C_error_R") > 0) | self.alarm_val("RCU_PCB_ID_R") ) def read_ANT_error_R(self): - return self.proxy.ANT_mask_RW & ( - ~self.proxy.RCU_ADC_locked_R + return self.read_attribute("ANT_mask_RW") & ( + ~self.read_attribute("RCU_ADC_locked_R") ) RECV_IOUT_error_R = attribute(dtype=(bool,), max_dim_x=32) @@ -276,7 +276,7 @@ class RECV(opcua_device): RECV_VOUT_error_R = attribute(dtype=(bool,), max_dim_x=32) def read_RECV_IOUT_error_R(self): - return (self.proxy.ANT_mask_RW & ( + return (self.read_attribute("ANT_mask_RW") & ( self.alarm_val("RCU_PWR_ANT_IOUT_R") )).any(axis=1) @@ -287,15 +287,15 @@ class RECV(opcua_device): ) def read_RECV_VOUT_error_R(self): - return (self.proxy.ANT_mask_RW & ( + return (self.read_attribute("ANT_mask_RW") & ( self.alarm_val("RCU_PWR_ANT_VIN_R") | self.alarm_val("RCU_PWR_ANT_VOUT_R") - )).any(axis=1) | (self.proxy.RCU_mask_RW & ( + )).any(axis=1) | (self.read_attribute("RCU_mask_RW") & ( self.alarm_val("RCU_PWR_1V8_R") | self.alarm_val("RCU_PWR_2V5_R") | self.alarm_val("RCU_PWR_3V3_R") - | ~self.proxy.RCU_PWR_DIGITAL_on_R - | ~self.proxy.RCU_PWR_good_R + | ~self.read_attribute("RCU_PWR_DIGITAL_on_R") + | ~self.read_attribute("RCU_PWR_good_R") )) # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py index bedae17dac39d06b39635d2ee9a2fed508fac5f4..c4dc14a165f69ce471e7c416fef14dd0771b54cb 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py @@ -166,27 +166,27 @@ class SDP(opcua_device): # ---------- # Summarising Attributes # ---------- - FPGA_error_R = attribute(dtype=(bool,), max_dim_x=16) - FPGA_processing_error_R = attribute(dtype=(bool,), max_dim_x=16) - FPGA_input_error_R = attribute(dtype=(bool,), max_dim_x=16) + FPGA_error_R = attribute(dtype=(bool,), max_dim_x=16, fisallowed="is_attribute_wrapper_allowed") + FPGA_processing_error_R = attribute(dtype=(bool,), max_dim_x=16, fisallowed="is_attribute_wrapper_allowed") + FPGA_input_error_R = attribute(dtype=(bool,), max_dim_x=16, fisallowed="is_attribute_wrapper_allowed") def read_FPGA_error_R(self): - return self.proxy.TR_fpga_mask_RW & ( - self.proxy.TR_fpga_communication_error_R - | (self.proxy.FPGA_firmware_version_R != "") - | (self.proxy.FPGA_jesd204b_csr_dev_syncn_R == 0).any(axis=1) + return self.read_attribute("TR_fpga_mask_R") & ( + self.read_attribute("TR_fpga_communication_error_R") + | (self.read_attribute("FPGA_firmware_version_R") != "") + | (self.read_attribute("FPGA_jesd204b_csr_dev_syncn_R") == 0).any(axis=1) ) def read_FPGA_processing_error_R(self): - return self.proxy.TR_fpga_mask_RW & ( - ~self.proxy.FPGA_processing_enable_R - | (self.proxy.FPGA_boot_image_R == 0) + return self.read_attribute("TR_fpga_mask_R") & ( + ~self.read_attribute("FPGA_processing_enable_R") + | (self.read_attribute("FPGA_boot_image_R") == 0) ) def read_FPGA_input_error_R(self): - return self.proxy.TR_fpga_mask_RW & ( - self.proxy.FPGA_wg_enable_R.any(axis=1) - | (self.proxy.FPGA_signal_input_rms_R == 0).any(axis=1) + return self.read_attribute("TR_fpga_mask_R") & ( + self.read_attribute("FPGA_wg_enable_R").any(axis=1) + | (self.read_attribute("FPGA_signal_input_rms_R") == 0).any(axis=1) ) # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/snmp_device.py b/tangostationcontrol/tangostationcontrol/devices/snmp_device.py new file mode 100644 index 0000000000000000000000000000000000000000..04d5a1425e19b0c5fbcb076f206bcd4ed122618a --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/snmp_device.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# This file is part of theRECV project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" SNMP Device for LOFAR2.0 + +""" + +# PyTango imports +from tango.server import run +from tango.server import device_property +from tango import AttrWriteType + +# Additional import +from tangostationcontrol.clients.snmp_client import SNMP_client +from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper +from tangostationcontrol.devices.lofar_device import lofar_device + +import numpy + +import logging +logger = logging.getLogger() + +__all__ = ["SNMP", "main"] + + +class SNMP(lofar_device): + """ + + **Properties:** + + - Device Property + SNMP_community + - Type:'DevString' + SNMP_host + - Type:'DevULong' + SNMP_timeout + - Type:'DevDouble' + """ + + # ----------------- + # Device Properties + # ----------------- + + SNMP_community = device_property( + dtype='DevString', + mandatory=True + ) + + SNMP_host = device_property( + dtype='DevString', + mandatory=True + ) + + SNMP_timeout = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + + + # octetstring + sysDescr_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysDescr"}, datatype=numpy.str) + sysName_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysName"}, datatype=numpy.str) + + # get a table element with the oid + ifDescr31_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.2.2.1.2.31"}, datatype=numpy.str) + + # get 10 table elements with the oid and dimension + ifDescr_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.2.2.1.2"}, dims=(10,), datatype=numpy.str) + + #timeticks + sysUpTime_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysUpTime"}, datatype=numpy.int64) + + # OID + sysObjectID_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysObjectID"}, datatype=numpy.int64) + + # integer + sysServices_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysServices"}, datatype=numpy.int64) + tcpRtoAlgorithm_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "tcpRtoAlgorithm"}, datatype=numpy.int64) + snmpEnableAuthenTraps_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "snmpEnableAuthenTraps"}, datatype=numpy.int64) + + #gauge + tcpCurrEstab_R = attribute_wrapper(comms_annotation={"mib": "RFC1213-MIB", "name": "tcpCurrEstab"}, datatype=numpy.int64) + + #counter32 + tcpActiveOpens_R = attribute_wrapper(comms_annotation={"mib": "RFC1213-MIB", "name": "tcpActiveOpens"}, datatype=numpy.int64) + snmpInPkts_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "snmpInPkts"}, datatype=numpy.int64) + + #IP address + ipAdEntAddr_R = attribute_wrapper(comms_annotation={"mib": "RFC1213-MIB", "name": "ipAdEntAddr", "index": (127,0,0,1)}, datatype=numpy.str) + ipAdEntIfIndex_R = attribute_wrapper(comms_annotation={"mib": "RFC1213-MIB", "name": "ipAdEntIfIndex", "index": (10, 87, 6, 14)}, datatype=numpy.str) + + #str RW attribute + sysContact_obj_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysContact"}, datatype=numpy.str) + sysContact_obj_RW = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysContact"}, datatype=numpy.str, access=AttrWriteType.READ_WRITE) + + + + # -------- + # overloaded functions + # -------- + def configure_for_initialise(self): + """ user code here. is called when the state is set to STANDBY """ + + # set up the SNMP ua client + self.snmp_manager = SNMP_client(self.SNMP_community, self.SNMP_host, self.SNMP_timeout, self.Fault, self) + + # map an access helper class + for i in self.attr_list(): + try: + i.set_comm_client(self.snmp_manager) + except Exception as e: + # use the pass function instead of setting read/write fails + i.set_pass_func() + logger.warning("error while setting the SNMP attribute {} read/write function. {}".format(i, e)) + + self.snmp_manager.start() + + +# -------- +# Commands +# -------- + + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the module.""" + + from tangostationcontrol.common.lofar_logging import configure_logger + configure_logger() + + return run((SNMP,), args=args, **kwargs) diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py index 48b5c3147507cc2b939c88ac303854f823c7ecb8..614055a716ce8a527197aaad159273ebfda220ef 100644 --- a/tangostationcontrol/tangostationcontrol/devices/unb2.py +++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py @@ -130,23 +130,23 @@ class UNB2(opcua_device): # ---------- # Summarising Attributes # ---------- - UNB2_error_R = attribute(dtype=(bool,), max_dim_x=2) + UNB2_error_R = attribute(dtype=(bool,), max_dim_x=2, fisallowed="is_attribute_wrapper_allowed") def read_UNB2_error_R(self): - return self.proxy.UNB2_mask_RW & ( - (self.proxy.UNB2TR_I2C_bus_error_R > 0) + return self.read_attribute("UNB2_mask_RW") & ( + (self.read_attribute("UNB2TR_I2C_bus_error_R") > 0) | self.alarm_val("UNB2_PCB_ID_R") - | (self.proxy.UNB2TR_I2C_bus_DDR4_error_R > 0).any(axis=1) - | (self.proxy.UNB2TR_I2C_bus_FPGA_PS_error_R > 0).any(axis=1) - | (self.proxy.UNB2TR_I2C_bus_QSFP_error_R > 0).any(axis=1) + | (self.read_attribute("UNB2TR_I2C_bus_DDR4_error_R") > 0).any(axis=1) + | (self.read_attribute("UNB2TR_I2C_bus_FPGA_PS_error_R") > 0).any(axis=1) + | (self.read_attribute("UNB2TR_I2C_bus_QSFP_error_R") > 0).any(axis=1) ) - UNB2_IOUT_error_R = attribute(dtype=(bool,), max_dim_x=2) - UNB2_TEMP_error_R = attribute(dtype=(bool,), max_dim_x=2) - UNB2_VOUT_error_R = attribute(dtype=(bool,), max_dim_x=2) + UNB2_IOUT_error_R = attribute(dtype=(bool,), max_dim_x=2, fisallowed="is_attribute_wrapper_allowed") + UNB2_TEMP_error_R = attribute(dtype=(bool,), max_dim_x=2, fisallowed="is_attribute_wrapper_allowed") + UNB2_VOUT_error_R = attribute(dtype=(bool,), max_dim_x=2, fisallowed="is_attribute_wrapper_allowed") def read_UNB2_IOUT_error_R(self): - return self.proxy.UNB2_mask_RW & ( + return self.read_attribute("UNB2_mask_RW") & ( self.alarm_val("UNB2_DC_DC_48V_12V_IOUT_R") | self.alarm_val("UNB2_FPGA_POL_CORE_IOUT_R").any(axis=1) | self.alarm_val("UNB2_FPGA_POL_ERAM_IOUT_R").any(axis=1) @@ -178,7 +178,7 @@ class UNB2(opcua_device): ) def read_UNB2_VOUT_error_R(self): - return self.proxy.UNB2_mask_RW & ( + return self.read_attribute("UNB2_mask_RW") & ( self.alarm_val("UNB2_DC_DC_48V_12V_VOUT_R") | self.alarm_val("UNB2_FPGA_POL_CORE_VOUT_R").any(axis=1) | self.alarm_val("UNB2_FPGA_POL_ERAM_VOUT_R").any(axis=1) diff --git a/tangostationcontrol/tangostationcontrol/examples/snmp/snmp.py b/tangostationcontrol/tangostationcontrol/examples/snmp/snmp.py deleted file mode 100644 index 3c962da9911abf9a0b9cbb4d7ecd4ff19c6e95d5..0000000000000000000000000000000000000000 --- a/tangostationcontrol/tangostationcontrol/examples/snmp/snmp.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of theRECV project -# -# -# -# Distributed under the terms of the APACHE license. -# See LICENSE.txt for more info. - -""" SNMP Device for LOFAR2.0 - -""" - -# PyTango imports -from tango.server import run -from tango.server import device_property -from tango import AttrWriteType - -# Additional import -from tangostationcontrol.examples.snmp.snmp_client import SNMP_client -from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper -from tangostationcontrol.devices.lofar_device import lofar_device - -import numpy - -import logging -logger = logging.getLogger() - -__all__ = ["SNMP", "main"] - - -class SNMP(lofar_device): - """ - - **Properties:** - - - Device Property - SNMP_community - - Type:'DevString' - SNMP_host - - Type:'DevULong' - SNMP_timeout - - Type:'DevDouble' - """ - - # ----------------- - # Device Properties - # ----------------- - - SNMP_community = device_property( - dtype='DevString', - mandatory=True - ) - - SNMP_host = device_property( - dtype='DevString', - mandatory=True - ) - - SNMP_timeout = device_property( - dtype='DevDouble', - mandatory=True - ) - - # ---------- - # Attributes - # ---------- - - sys_description_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.1.0"}, datatype=numpy.str) - sys_objectID_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.2.0", "type": "OID"}, datatype=numpy.str) - sys_uptime_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.3.0", "type": "TimeTicks"}, datatype=numpy.int64) - sys_name_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.5.0"}, datatype=numpy.str) - ip_route_mask_127_0_0_1_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.4.21.1.11.127.0.0.1", "type": "IpAddress"}, datatype=numpy.str) - TCP_active_open_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.5.0", "type": "Counter32"}, datatype=numpy.int64) - - sys_contact_RW = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str, access=AttrWriteType.READ_WRITE) - sys_contact_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str) - - TCP_Curr_estab_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.9.0", "type": "Gauge"}, datatype=numpy.int64) - - # inferred spectrum - if_index_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.2.2.1.1"}, dims=(10,), datatype=numpy.int64) - - - # -------- - # overloaded functions - # -------- - def configure_for_initialise(self): - """ user code here. is called when the state is set to STANDBY """ - - # set up the SNMP ua client - self.snmp_manager = SNMP_client(self.SNMP_community, self.SNMP_host, self.SNMP_timeout, self.Fault, self) - - # map an access helper class - for i in self.attr_list(): - try: - i.set_comm_client(self.snmp_manager) - except Exception as e: - # use the pass function instead of setting read/write fails - i.set_pass_func() - logger.warning("error while setting the SNMP attribute {} read/write function. {}".format(i, e)) - - self.snmp_manager.start() - - -# -------- -# Commands -# -------- - - -# ---------- -# Run server -# ---------- -def main(args=None, **kwargs): - """Main function of the module.""" - - from tangostationcontrol.common.lofar_logging import configure_logger - configure_logger() - - return run((SNMP,), args=args, **kwargs) diff --git a/tangostationcontrol/tangostationcontrol/examples/snmp/snmp_client.py b/tangostationcontrol/tangostationcontrol/examples/snmp/snmp_client.py deleted file mode 100644 index 9a0919457bb692c614a5edc6f425664202435d9b..0000000000000000000000000000000000000000 --- a/tangostationcontrol/tangostationcontrol/examples/snmp/snmp_client.py +++ /dev/null @@ -1,163 +0,0 @@ - -from tangostationcontrol.clients.comms_client import CommClient - -import snmp - -import numpy -import logging - -logger = logging.getLogger() - -__all__ = ["SNMP_client"] - - -snmp_to_numpy_dict = { - snmp.types.INTEGER: numpy.int64, - snmp.types.TimeTicks: numpy.int64, - snmp.types.OCTET_STRING: numpy.str, - snmp.types.OID: numpy.str, - snmp.types.Counter32: numpy.int64, - snmp.types.Gauge32: numpy.int64, - snmp.types.IpAddress: numpy.str, -} - -snmp_types = { - "Integer": numpy.int64, - "Gauge": numpy.int64, - "TimeTick": numpy.int64, - "Counter32": numpy.int64, - "OctetString": numpy.str, - "IpAddress": numpy.str, - "OID": numpy.str, -} - - -class SNMP_client(CommClient): - """ - messages to keep a check on the connection. On connection failure, reconnects once. - """ - - def start(self): - super().start() - - def __init__(self, community, host, timeout, fault_func, try_interval=2): - """ - Create the SNMP and connect() to it - """ - super().__init__(fault_func, try_interval) - - self.community = community - self.host = host - self.manager = snmp.Manager(community=bytes(community, "utf8")) - - # Explicitly connect - if not self.connect(): - # hardware or infra is down -- needs fixing first - fault_func() - return - - def connect(self): - """ - Try to connect to the client - """ - logger.debug(f"Connecting to community: {self.community}, host: {self.host}") - - self.connected = True - return True - - def ping(self): - """ - ping the client to make sure the connection with the client is still functional. - """ - pass - - def _setup_annotation(self, annotation): - """ - This class's Implementation of the get_mapping function. returns the read and write functions - """ - - if isinstance(annotation, dict): - # check if required path inarg is present - if annotation.get('oids') is None: - ValueError("SNMP get attributes require an oid") - oids = annotation.get("oids") # required - else: - TypeError("SNMP attributes require a dict with oid(s)") - return - - dtype = annotation.get('type', None) - - return oids, dtype - - def setup_value_conversion(self, attribute): - """ - gives the client access to the attribute_wrapper object in order to access all data it could potentially need. - """ - - dim_x = attribute.dim_x - dim_y = attribute.dim_y - dtype = attribute.numpy_type - - return dim_x, dim_y, dtype - - def get_oids(self, x, y, in_oid): - - if x == 0: - x = 1 - if y == 0: - y = 1 - - nof_oids = x * y - - if nof_oids == 1: - # 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 - - elif type(in_oid) is list and len(in_oid) == nof_oids: - # already is an array and of the right length - return in_oid - elif type(in_oid) is list and len(in_oid) != nof_oids: - # already is an array but the wrong length. Unable to handle this - 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)) - else: - - return ["{}.{}".format(in_oid, i + 1) for i in range(nof_oids)] - - - def setup_attribute(self, annotation, attribute): - """ - MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions - """ - - # process the annotation - oids, dtype = self._setup_annotation(annotation) - - # get all the necessary data to set up the read/write functions from the attribute_wrapper - dim_x, dim_y, numpy_type = self.setup_value_conversion(attribute) - oids = self.get_oids(dim_x, dim_y, oids) - - def _read_function(): - vars = self.manager.get(self.host, *oids) - return [snmp_to_numpy_dict[type(i.value)](str(i.value)) for i in vars] - - if dtype is not None: - def _write_function(value): - if len(oids) == 1 and type(value) != list: - value = [value] - - for i in range(len(oids)): - self.manager.set(self.host, oids[i], snmp_types[dtype](value[i])) - else: - def _write_function(value): - if len(oids) == 1 and type(value) != list: - value = [value] - - for i in range(len(oids)): - self.manager.set(self.host, oids[i], value[i]) - - - # return the read/write functions - return _read_function, _write_function diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py new file mode 100644 index 0000000000000000000000000000000000000000..f061e38cedc7cefefeb72976454edecd7b647259 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py @@ -0,0 +1,242 @@ +from pysnmp import hlapi +import numpy + +from unittest import mock + +from tangostationcontrol.test import base + +from tangostationcontrol.clients.snmp_client import SNMP_client, snmp_attribute, annotation_wrapper + + +class server_imitator: + # conversion dict + snmp_to_numpy_dict = { + hlapi.Integer32: numpy.int64, + hlapi.TimeTicks: numpy.int64, + str: str, + hlapi.Counter32: numpy.int64, + hlapi.Gauge32: numpy.int64, + hlapi.IpAddress: str, + } + + # shortcut for testing dimensionality + dim_list = { + "scalar": (1, 0), + "spectrum": (4, 0), + } + + def get_return_val(self, snmp_type : type, dims : tuple): + """ + provides the return value for the set/get functions that an actual server would return. + """ + + if dims == self.dim_list["scalar"]: + if snmp_type is hlapi.ObjectIdentity: + read_val = (None, snmp_type("1.3.6.1.2.1.1.1.0")) + elif snmp_type is hlapi.IpAddress: + read_val = (None, snmp_type("1.1.1.1")) + else: + read_val = (None, snmp_type(1)) + + + elif dims == self.dim_list["spectrum"]: + if snmp_type is hlapi.ObjectIdentity: + read_val = [] + for _i in range(dims[0]): + read_val.append((None, snmp_type(f"1.3.6.1.2.1.1.1.0.1"))) + elif snmp_type is hlapi.IpAddress: + read_val = [] + for _i in range(dims[0]): + read_val.append((None, snmp_type(f"1.1.1.1"))) + else: + read_val = [] + for _i in range(dims[0]): + read_val.append((None, snmp_type(1))) + else: + raise Exception("Image not yet supported :(") + + return read_val + + + def val_check(self, snmp_type : type, dims : tuple): + """ + provides the values we expect and would provide to the attribute after converting the + """ + + if dims == self.dim_list["scalar"]: + if snmp_type is hlapi.ObjectIdentity: + check_val = "1.3.6.1.2.1.1.1.0.1" + elif snmp_type is hlapi.IpAddress: + check_val = "1.1.1.1" + elif snmp_type is str: + check_val = "1" + else: + check_val = 1 + elif dims == self.dim_list["spectrum"]: + if snmp_type is hlapi.ObjectIdentity: + check_val = ["1.3.6.1.2.1.1.1.0.1"] * dims[0] + + elif snmp_type is hlapi.IpAddress: + check_val = ["1.1.1.1"] * dims[0] + elif snmp_type is str: + check_val = ["1"] * dims[0] + else: + check_val = [1] * dims[0] + else: + raise Exception("Image not yet supported :(") + + return check_val + +class TestSNMP(base.TestCase): + + + def test_annotation_success(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) + + test_list = [ + # test name nad MIB type annotation + {"mib": "SNMPv2-MIB", "name": "sysDescr"}, + + # 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}, + + #oid + {"oids": "1.3.6.1.2.1.2.2.1.2.31"} + ] + + + for i in test_list: + wrapper = client._setup_annotation(annotation=i) + + if wrapper.oids is not None: + self.assertEqual(wrapper.oids, i["oids"]) + + 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}") + + + 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) + + def test_oids_scalar(self): + + test_oid = "1.1.1.1" + + server = server_imitator() + + x, y = server.dim_list['scalar'] + + # we just need the object to call another function + wrapper = annotation_wrapper(annotation = {"oids": "Not None lol"}) + # scalar + scalar_expected = [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): + """ + Tests the "get_oids" function, which is for getting lists of sequential oids. + + Results should basically be an incrementing list of oids with the final number incremented by 1 each time. + So "1.1" with dims of 3x1 might become ["1.1.1", "1.1.2", "1.1.3"] + """ + server = server_imitator() + + test_oid = "1.1.1.1" + x, y = server.dim_list['spectrum'] + + # we just need the object to call another function + wrapper = annotation_wrapper(annotation={"oids": "Not None lol"}) + + # spectrum + spectrum_expected = [test_oid + ".1", test_oid + ".2", test_oid + ".3", test_oid + ".4"] + ret_oids = wrapper._get_oids(x, y, test_oid) + self.assertListEqual(ret_oids, spectrum_expected, f"Expected: {spectrum_expected}, got: {ret_oids}") + + @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): + """ + Attempts to read a fake SNMP variable and checks whether it got what it expected + """ + + server = server_imitator() + + for j in server.dim_list: + for i in server.snmp_to_numpy_dict: + m_next.return_value = (None, None, None, server.get_return_val(i, server.dim_list[j])) + + m_client = mock.Mock() + + + wrapper = annotation_wrapper(annotation={"oids": "1.3.6.1.2.1.2.2.1.2.31"}) + snmp_attr = snmp_attribute(client=m_client, wrapper=wrapper, dtype=server.snmp_to_numpy_dict[i], dim_x=server.dim_list[j][0], dim_y=server.dim_list[j][1]) + + val = snmp_attr.read_function() + + checkval = server.val_check(i, server.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. + """ + server = server_imitator() + + + for j in server.dim_list: + for i in server.snmp_to_numpy_dict: + m_next.return_value = (None, None, None, server.get_return_val(i, server.dim_list[j])) + + m_client = mock.Mock() + set_val = server.val_check(i, server.dim_list[j]) + + wrapper = annotation_wrapper(annotation={"oids": "1.3.6.1.2.1.2.2.1.2.31"}) + snmp_attr = snmp_attribute(client=m_client, wrapper=wrapper, dtype=server.snmp_to_numpy_dict[i], dim_x=server.dim_list[j][0], dim_y=server.dim_list[j][1]) + + res_lst = [] + def test(*value): + res_lst.append(value[1]) + return None, None, None, server.get_return_val(i, server.dim_list[j]) + + hlapi.ObjectType = test + + snmp_attr.write_function(set_val) + + if len(res_lst) == 1: + res_lst = res_lst[0] + + checkval = server.val_check(i, server.dim_list[j]) + self.assertEqual(checkval, res_lst, f"Expected: {checkval}, got: {res_lst}") + + + + diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py new file mode 100644 index 0000000000000000000000000000000000000000..46004707ea59c681015b987ce97adb26931a189a --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from tango.test_context import DeviceTestContext +from tango.server import attribute + +from tangostationcontrol.devices import lofar_device + +import mock + +from tangostationcontrol.test import base + +class TestLofarDevice(base.TestCase): + def setUp(self): + super(TestLofarDevice, self).setUp() + + # Patch DeviceProxy to allow making the proxies during initialisation + # that we otherwise avoid using + for device in [lofar_device]: + proxy_patcher = mock.patch.object( + device, 'DeviceProxy') + proxy_patcher.start() + self.addCleanup(proxy_patcher.stop) + + def test_read_attribute(self): + """ Test whether read_attribute really returns the attribute. """ + + class MyLofarDevice(lofar_device.lofar_device): + @attribute(dtype=float) + def A(self): + return 42.0 + + @attribute(dtype=float) + def read_attribute_A(self): + return self.read_attribute("A") + + @attribute(dtype=(float,), max_dim_x=2) + def B_array(self): + return [42.0, 43.0] + + @attribute(dtype=(float,), max_dim_x=2) + def read_attribute_B_array(self): + return self.read_attribute("B_array") + + with DeviceTestContext(MyLofarDevice, process=True, timeout=10) as proxy: + proxy.initialise() + self.assertEqual(42.0, proxy.read_attribute_A) + self.assertListEqual([42.0, 43.0], proxy.read_attribute_B_array.tolist()) diff --git a/tangostationcontrol/tangostationcontrol/toolkit/TODO_HdbppPython.md b/tangostationcontrol/tangostationcontrol/toolkit/TODO_HdbppPython.md new file mode 100644 index 0000000000000000000000000000000000000000..a72618b3fffac8b4c94f6b46d4bb1353db637c67 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/toolkit/TODO_HdbppPython.md @@ -0,0 +1,47 @@ +# To Do List + +## Updates to incorporate Hdbpp-Python as our Retriever + +The library in [libhdbpp-python](https://gitlab.com/tango-controls/hdbpp/libhdbpp-python) implements an AbstractReader and multiple Readers (i.e. what is called Retriever in our repo) following the relative hdb++ engine. Currently (March 2022), only MariaDB implementation is on the master branch, while Timescaledb is on development in [libhdbpp-python-timescaledb-branch](https://gitlab.com/tango-controls/hdbpp/libhdbpp-python/-/tree/package_and_timescaledb_support). + +### Approach +The Reader relies upon hard-coded SQL scripts inside the Python methods, managed as strings. +Even our first version of the Retriever used this approach, but then we decided to overcome the hardcoded SQL scripts using the SQLAlchemy Python library, which has led to a more stable, reliable and customizable code. + +### Compatibility +The libhdbpp-reader is compatible with our code and our archiver setup, as demonstrated in [demonstrator](../../../jupyter-notebooks/HdbppReader_demonstrator.ipynb). + +### Functionalities in libhdbpp-python-reader +These are the functionalities implemented in the libhdbpp-reader: +- get_connection() : Return the connection object to avoid a client to open one for custom queries. +- get_attributes(active=False, pattern='') : Queries the database for the current list of archived attributes +- is_attribute_archived(attribute, active=False): Returns if an attribute has values in DB +- get_last_attribute_value(attribute) : Returns last value inserted in DB for an attribute +- get_last_attributes_values(attributes, columns = 'time, r_value'): Returns last values inserted in DB for a list of attributes +- get_attribute_values(attribute,start_date, stop_date=None,decimate=None,**params): Returns attribute values between start and stop dates +- get_attributes_values(attributes,start_date, stop_date=None,decimate=None,correlate = False,columns = 'time, r_value',**params): Returns attributes values between start and stop dates, using decimation or not, correlating the values or not. + +### TODO List for our repository +The Reader in libhdpp-python has roughly the same functionalities of ours, but if we need to align our methods to the AbstractReader, we must: +- replace the methods/parameter names +- introduce the concept of active Attribute (boolean flag that indicates if the attribute is being currently archived) +- add the decimate parameter (avg, count, etc..) +- add the correlation parameter ('if True, data is generated so that there is available data for each timestamp of each attribute') +- add a more general pattern parameter to retrieve methods + +### TODO List for libhdbpp-python in case of our contribuition +Since we experimented that SQLAlchemy Python library adds many more benefits compared to the use of bare SQL-strings, in case of our contribuition to the libhdbpp-python, these functionalities must be added to the tango-repository: +- install and import SQLAlchemy library +- development of an ArchiverBase class that maps the DBMS-schema (we've had alredy both the TimescaleDB and MariaDB version) +- replacement of the SQL-strings in Reader methods with classes/methods representing relative DB tables/scripts +- Add some small extra functionalities to match our developed methods (get_attribute_format(), get_attribute_tablename(), etc...) + +### Linting and other general issues to be fixed in libhdbpp-python code +If we want that pipeline doesn't raise errors we need to fix the following errors: +- imported but unused packages (F401) +- use of bare exceptions (B001) +- use of mutable data structures for argument defaults (B006) +- Xenon complexity of some methods (acceptable for now) + + +