Skip to content
Snippets Groups Projects
Commit 0399856a authored by Taya Snijder's avatar Taya Snijder
Browse files

Merge branch 'L2SS-679_initial_pdu_support' into 'master'

Resolve L2SS-679 "Initial pdu support"

Closes L2SS-679

See merge request !359
parents 05d73d32 93246b45
No related branches found
No related tags found
1 merge request!359Resolve L2SS-679 "Initial pdu support"
Showing
with 91438 additions and 239 deletions
...@@ -44,7 +44,25 @@ ...@@ -44,7 +44,25 @@
"PDU": { "PDU": {
"STAT": { "STAT": {
"PDU": { "PDU": {
"STAT/PDU/1": {} "STAT/PDU/1": {
"properties": {
"SNMP_host": ["10.87.2.145"],
"SNMP_community": ["public"],
"SNMP_mib_dir": ["devices/psoc_mib/PowerNet-MIB.mib"],
"SNMP_timeout": ["10.0"],
"SNMP_version": ["1"],
"PSOC_sockets": [
"socket_1",
"socket_2",
"socket_3",
"socket_4",
"socket_5",
"socket_6",
"socket_7",
"socket_8"
]
}
}
} }
} }
}, },
......
...@@ -65,5 +65,4 @@ console_scripts = ...@@ -65,5 +65,4 @@ console_scripts =
l2ss-version = tangostationcontrol.common.lofar_version:main l2ss-version = tangostationcontrol.common.lofar_version:main
[options.package_data] [options.package_data]
* = *.json * = *.json, *.mib
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
from tangostationcontrol.clients.comms_client import CommClient from tangostationcontrol.clients.comms_client import CommClient
from pysnmp import hlapi from pysnmp import hlapi
from pysnmp.smi import builder from pysnmp.smi import builder, compiler
from os import path from os import path
import numpy import numpy
import logging import logging
...@@ -29,20 +30,28 @@ class SNMP_comm: ...@@ -29,20 +30,28 @@ class SNMP_comm:
Holds information for communicating with the SNMP server Holds information for communicating with the SNMP server
""" """
def __init__(self, community, host, port): def __init__(self, community, host, version, port=161):
if version not in [1, 2, 3]:
raise ValueError("SNMP version can only be 1, 2 or 3")
# pysnmp for whatever reason starts counting versions at 0.
pysnmp_snmp_version = version - 1
self.port = port self.port = port
self.engine = hlapi.SnmpEngine() self.engine = hlapi.SnmpEngine()
self.community = hlapi.CommunityData(community) self.community = hlapi.CommunityData(community, mpModel=pysnmp_snmp_version)
self.transport = hlapi.UdpTransportTarget((host, port)) self.transport = hlapi.UdpTransportTarget((host, port))
# context data sets the version used. Default SNMPv2 # context data sets the version used. Default SNMPv2
self.ctx_data = hlapi.ContextData() self.ctx_data = hlapi.ContextData()
def getter(self, objs): def getter(self, objs):
return next(hlapi.getCmd(self.engine, self.community, self.transport, self.ctx_data, *objs)) return next(hlapi.getCmd(self.engine, self.community, self.transport, self.ctx_data, *objs))
def setter(self, objs): def setter(self, objs):
return next(hlapi.getCmd(self.engine, self.community, self.transport, self.ctx_data, *objs)) return next(hlapi.setCmd(self.engine, self.community, self.transport, self.ctx_data, *objs))
class SNMP_client(CommClient): class SNMP_client(CommClient):
...@@ -53,14 +62,14 @@ class SNMP_client(CommClient): ...@@ -53,14 +62,14 @@ class SNMP_client(CommClient):
def start(self): def start(self):
super().start() super().start()
def __init__(self, community, host, timeout, fault_func, try_interval=2, port=161): def __init__(self, community, host, timeout, version, fault_func, try_interval=2, port=161):
""" """
Create the SNMP engine Create the SNMP engine
""" """
super().__init__(fault_func, try_interval) super().__init__(fault_func, try_interval)
logger.debug(f"setting up SNMP engine with host: {host} and community: {community}") logger.debug(f"setting up SNMP engine with host: {host} and community: {community}")
self.SNMP_comm = SNMP_comm(community, host, port) self.SNMP_comm = SNMP_comm(community, host, version, port)
# only sets up the engine, doesn't connect # only sets up the engine, doesn't connect
self.connected = True self.connected = True
...@@ -103,7 +112,7 @@ class SNMP_client(CommClient): ...@@ -103,7 +112,7 @@ class SNMP_client(CommClient):
# get all the necessary data to set up the read/write functions from the attribute_wrapper # 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) dim_x, dim_y, dtype = self.setup_value_conversion(attribute)
snmp_attr = snmp_attribute(self, mib, name, idx, dtype, dim_x, dim_y) snmp_attr = snmp_attribute(self.SNMP_comm, mib, name, idx, dtype, dim_x, dim_y)
# return the read/write functions # return the read/write functions
def read_function(): def read_function():
...@@ -127,7 +136,8 @@ class snmp_attribute: ...@@ -127,7 +136,8 @@ class snmp_attribute:
self.len = self.get_len(dim_x, dim_y) self.len = self.get_len(dim_x, dim_y)
self.is_scalar = self.len == 1 self.is_scalar = self.len == 1
self.objID = self.create_objID() self.objID = tuple(hlapi.ObjectIdentity(self.mib, self.name, self.idx + i) for i in range(self.len))
self.objs = tuple((hlapi.ObjectType(i) for i in self.objID), )
def get_len(self, dim_x, dim_y): def get_len(self, dim_x, dim_y):
"""""Small helper function to not clutter the __init__""" """""Small helper function to not clutter the __init__"""
...@@ -138,24 +148,13 @@ class snmp_attribute: ...@@ -138,24 +148,13 @@ class snmp_attribute:
dim_y = 1 dim_y = 1
return dim_x * dim_y return dim_x * dim_y
def create_objID(self):
if self.is_scalar:
objID = hlapi.ObjectIdentity(self.mib, self.name, self.idx)
else:
objID = tuple(hlapi.ObjectIdentity(self.mib, self.name, self.idx + i) for i in range(self.len))
return objID
def read_function(self): def read_function(self):
""" """
Read function we give to the attribute wrapper Read function we give to the attribute wrapper
""" """
# we need to remake this every time it seems or things dont work
self.objs = tuple(hlapi.ObjectType(i) for i in self.objID)
# get all of the values # get all of the values
errorIndication, errorStatus, errorIndex, *varBinds = self.comm.getter(*self.objs) errorIndication, errorStatus, errorIndex, *varBinds = self.comm.getter(self.objs)
# get all the values in a list converted to the correct type # get all the values in a list converted to the correct type
val_lst = self.convert(varBinds) val_lst = self.convert(varBinds)
...@@ -168,14 +167,14 @@ class snmp_attribute: ...@@ -168,14 +167,14 @@ class snmp_attribute:
Write function we give to the attribute wrapper Write function we give to the attribute wrapper
""" """
if self.is_scalar: if self.is_scalar and type(value) != list:
write_obj = tuple(hlapi.ObjectType(self.objID[0], value), ) value = [value]
else:
write_obj = tuple(hlapi.ObjectType(self.objID[i], value[i]) for i in range(len(self.objID))) write_obj = tuple(hlapi.ObjectType(self.objID[i], value[i]) for i in range(len(self.objID)))
errorIndication, errorStatus, errorIndex, *varBinds = self.comm.setter(write_obj) errorIndication, errorStatus, errorIndex, *varBinds = self.comm.setter(write_obj)
def convert(self, varBinds): def convert(self, varBinds):
""" """
get all the values in a list, make sure to convert specific types that dont want to play nicely get all the values in a list, make sure to convert specific types that dont want to play nicely
...@@ -183,8 +182,6 @@ class snmp_attribute: ...@@ -183,8 +182,6 @@ class snmp_attribute:
vals = [] vals = []
if not self.is_scalar:
#just the first element of this single element list
varBinds = varBinds[0] varBinds = varBinds[0]
for varBind in varBinds: for varBind in varBinds:
...@@ -234,8 +231,11 @@ class mib_loader: ...@@ -234,8 +231,11 @@ class mib_loader:
if not path.isabs(mib_dir): if not path.isabs(mib_dir):
mib_dir = "/" + mib_dir mib_dir = "/" + mib_dir
mib_source = builder.DirMibSource(mib_dir) compiler.addMibCompiler(self.mibBuilder, sources=[f'file://{mib_dir}', ])
self.mibBuilder.addMibSources(mib_source) logger.debug(f"mib sources: {self.mibBuilder.getMibSources()}")
def load_pymib(self, mib_name): def load_pymib(self, mib_name):
self.mibBuilder.loadModule(mib_name) logger.debug(f"test {self.mibBuilder.getMibSources()}")
logger.debug(f"test {mib_name}")
self.mibBuilder.loadModules(mib_name)
...@@ -12,7 +12,20 @@ from tangostationcontrol.common.entrypoint import entry ...@@ -12,7 +12,20 @@ from tangostationcontrol.common.entrypoint import entry
from tangostationcontrol.devices.lofar_device import lofar_device from tangostationcontrol.devices.lofar_device import lofar_device
from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions
from tango.server import device_property, command
import os
import logging import logging
from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper
from tangostationcontrol.clients.snmp_client import SNMP_client, mib_loader, snmp_attribute
import numpy
import pkg_resources
from datetime import timedelta
from pysmi import debug
debug.setLogger(debug.Debug('searcher', "compiler", "borrower", "reader"))
logger = logging.getLogger() logger = logging.getLogger()
...@@ -24,22 +37,125 @@ class PDU(lofar_device): ...@@ -24,22 +37,125 @@ class PDU(lofar_device):
# ----------------- # -----------------
# Device Properties # Device Properties
# ----------------- # -----------------
SNMP_community = device_property(
dtype='DevString',
mandatory=True
)
SNMP_host = device_property(
dtype='DevString',
mandatory=True
)
SNMP_mib_dir = device_property(
dtype='DevString',
mandatory=True
)
SNMP_timeout = device_property(
dtype='DevDouble',
mandatory=True
)
SNMP_version = device_property(
dtype='DevULong',
mandatory=True
)
PSOC_sockets = device_property(
dtype=[str],
mandatory=True
)
# ---------- # ----------
# Attributes # Attributes
# ---------- # ----------
sockets_state_R = attribute_wrapper(comms_annotation={"mib": "PowerNet-MIB", "name": "sPDUOutletCtl", "index": 1}, dims=(8,), datatype=str)
master_state_R = attribute_wrapper(comms_annotation={"mib": "PowerNet-MIB", "name": "sPDUMasterState"}, datatype=str)
current_load_R = attribute_wrapper(comms_annotation={"mib": "PowerNet-MIB", "name": "rPDULoadStatusLoad", "index": 1}, datatype=numpy.int64)
uptime_R = attribute_wrapper(comms_annotation={"mib": "SNMPv2-MIB", "name": "sysUpTime"}, datatype=numpy.int64)
# -------- # --------
# overloaded functions # overloaded functions
# -------- # --------
def init_device(self): PSOC_NOF_SOCKETS = 8
super().init_device()
@log_exceptions() @log_exceptions()
def configure_for_initialise(self): def configure_for_initialise(self):
""" user code here. is called when the state is set to STANDBY """
# make sure all sockets are named
if len(self.PSOC_sockets) != self.PSOC_NOF_SOCKETS:
raise Exception(
f"At least {self.PSOC_NOF_SOCKETS} names are required to be given. You can simply leave any unused sockets as empty strings")
else:
# create a dict with the name of the sockets being keys for the socket number (e.g: "socket_nr_1": 1)
self.socket_dict = {self.PSOC_sockets[f]: f + 1 for f in range(len(self.PSOC_sockets))}
logger.debug(f"Configured PSOC with the following socket names: {self.PSOC_sockets}")
# set up the SNMP ua client
self.snmp_manager = SNMP_client(self.SNMP_community, self.SNMP_host, self.SNMP_timeout, self.SNMP_version, 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()
# prepares this object for the readable_uptime command
self.uptime_attr = snmp_attribute(self.snmp_manager.SNMP_comm, "SNMPv2-MIB", name="sysUpTime", idx=0, dtype=numpy.int64, dim_x=1, dim_y=0)
super().configure_for_initialise() super().configure_for_initialise()
def _toggle_socket(self, socket_name, on: bool):
"""
This function is tailored to the "APS switched rack PDU", changing the psoc will require some changes to this function
"""
try:
socket_nr = self.socket_dict[socket_name]
except Exception:
raise Exception(f"This is not a valid socket name, please make sure it is one of the following: {self.socket_dict.keys()}")
# get the correct value to set
if on:
socket_set = "outletOn"
else:
socket_set = "outletOff"
# create the snmp_attribute for the correct socket
attr = snmp_attribute(self.snmp_manager.SNMP_comm, "PowerNet-MIB", name="sPDUOutletCtl", idx=socket_nr, dtype=str, dim_x=1, dim_y=0)
# write the correct value
attr.write_function([socket_set])
@command(dtype_in=str)
def socket_on(self, socket_name):
self._toggle_socket(socket_name, on=True)
logger.debug(f"Turned socket {socket_name} on")
@command(dtype_in=str)
def socket_off(self, socket_name):
self._toggle_socket(socket_name, on=False)
logger.debug(f"Turned socket {socket_name} off")
@command(dtype_out=str)
def readable_uptime(self):
"""
This function returns a readable string of the uptime.
"""
# for whatever reason, the uptime is given in hundredts of a second
return str(timedelta(seconds=self.uptime_attr.read_function()/100))
@log_exceptions() @log_exceptions()
def configure_for_on(self): def configure_for_on(self):
super().configure_for_on() super().configure_for_on()
...@@ -48,6 +164,26 @@ class PDU(lofar_device): ...@@ -48,6 +164,26 @@ class PDU(lofar_device):
def configure_for_off(self): def configure_for_off(self):
super().configure_for_off() super().configure_for_off()
def get_mib_dir(self):
mib_filename_path = pkg_resources.resource_filename('tangostationcontrol', self.SNMP_mib_dir)
mib_path = os.path.dirname(mib_filename_path)
return mib_path
def init_device(self):
super().init_device()
# create the mib_loader and set the mib path
self.loader = mib_loader(self.get_mib_dir())
for i in self.attr_list():
try:
# for all of the attributes attempt to load the pre-compiled MIB. Skips already loaded ones
self.loader.load_pymib(i.comms_annotation["mib"])
except Exception as e:
raise Exception(
f"Failed to load MIB file: {i.comms_annotation.get('mib')} for attribute {i.name} in directory {self.get_mib_dir()} ") from e
# ---------- # ----------
# Run server # Run server
......
This diff is collapsed.
RFC-1212 DEFINITIONS ::= BEGIN
IMPORTS
ObjectName
FROM RFC1155-SMI
DisplayString
FROM RFC1158-MIB;
OBJECT-TYPE MACRO ::=
BEGIN
TYPE NOTATION ::=
-- must conform to
-- RFC1155's ObjectSyntax
"SYNTAX" type(ObjectSyntax)
"ACCESS" Access
"STATUS" Status
DescrPart
ReferPart
IndexPart
DefValPart
VALUE NOTATION ::= value (VALUE ObjectName)
Access ::= "read-only"
| "read-write"
| "write-only"
| "not-accessible"
Status ::= "mandatory"
| "optional"
| "obsolete"
| "deprecated"
DescrPart ::=
"DESCRIPTION" value (description DisplayString)
| empty
ReferPart ::=
"REFERENCE" value (reference DisplayString)
| empty
IndexPart ::=
"INDEX" "{" IndexTypes "}"
| empty
IndexTypes ::=
IndexType | IndexTypes "," IndexType
IndexType ::=
-- if indexobject, use the SYNTAX
-- value of the correspondent
-- OBJECT-TYPE invocation
value (indexobject ObjectName)
-- otherwise use named SMI type
-- must conform to IndexSyntax below
| type (indextype)
DefValPart ::=
"DEFVAL" "{" value (defvalue ObjectSyntax) "}"
| empty
END
IndexSyntax ::=
CHOICE {
number
INTEGER (0..MAX),
string
OCTET STRING,
object
OBJECT IDENTIFIER,
address
NetworkAddress,
ipAddress
IpAddress
}
END
RFC-1215 DEFINITIONS ::= BEGIN
-- This module is a empty module. It has been created solely for the
-- purpose of allowing other modules to correctly import the TRAP-TYPE
-- clause from RFC-1215 where it should be imported from. It's a
-- built in type in the UCD-SNMP code, and in fact RFC-1215 doesn't
-- actually define a mib at all; it only defines macros. However,
-- importing the TRAP-TYPE is conventionally done from an import
-- clause pointing to RFC-1215.
--
-- Wes 7/17/98
TRAP-TYPE MACRO ::=
BEGIN
TYPE NOTATION ::= "ENTERPRISE" value
(enterprise OBJECT IDENTIFIER)
VarPart
DescrPart
ReferPart
VALUE NOTATION ::= value (VALUE INTEGER)
VarPart ::=
"VARIABLES" "{" VarTypes "}"
| empty
VarTypes ::=
VarType | VarTypes "," VarType
VarType ::=
value (vartype ObjectName)
DescrPart ::=
"DESCRIPTION" value (description DisplayString)
| empty
ReferPart ::=
"REFERENCE" value (reference DisplayString)
| empty
END
END
RFC1155-SMI DEFINITIONS ::= BEGIN
EXPORTS -- EVERYTHING
internet, directory, mgmt,
experimental, private, enterprises,
OBJECT-TYPE, ObjectName, ObjectSyntax, SimpleSyntax,
ApplicationSyntax, NetworkAddress, IpAddress,
Counter, Gauge, TimeTicks, Opaque;
-- the path to the root
internet OBJECT IDENTIFIER ::= { iso org(3) dod(6) 1 }
directory OBJECT IDENTIFIER ::= { internet 1 }
mgmt OBJECT IDENTIFIER ::= { internet 2 }
experimental OBJECT IDENTIFIER ::= { internet 3 }
private OBJECT IDENTIFIER ::= { internet 4 }
enterprises OBJECT IDENTIFIER ::= { private 1 }
-- definition of object types
OBJECT-TYPE MACRO ::=
BEGIN
TYPE NOTATION ::= "SYNTAX" type (TYPE ObjectSyntax)
"ACCESS" Access
"STATUS" Status
VALUE NOTATION ::= value (VALUE ObjectName)
Access ::= "read-only"
| "read-write"
| "write-only"
| "not-accessible"
Status ::= "mandatory"
| "optional"
| "obsolete"
END
-- names of objects in the MIB
ObjectName ::=
OBJECT IDENTIFIER
-- syntax of objects in the MIB
ObjectSyntax ::=
CHOICE {
simple
SimpleSyntax,
-- note that simple SEQUENCEs are not directly
-- mentioned here to keep things simple (i.e.,
-- prevent mis-use). However, application-wide
-- types which are IMPLICITly encoded simple
-- SEQUENCEs may appear in the following CHOICE
application-wide
ApplicationSyntax
}
SimpleSyntax ::=
CHOICE {
number
INTEGER,
string
OCTET STRING,
object
OBJECT IDENTIFIER,
empty
NULL
}
ApplicationSyntax ::=
CHOICE {
address
NetworkAddress,
counter
Counter,
gauge
Gauge,
ticks
TimeTicks,
arbitrary
Opaque
-- other application-wide types, as they are
-- defined, will be added here
}
-- application-wide types
NetworkAddress ::=
CHOICE {
internet
IpAddress
}
IpAddress ::=
[APPLICATION 0] -- in network-byte order
IMPLICIT OCTET STRING (SIZE (4))
Counter ::=
[APPLICATION 1]
IMPLICIT INTEGER (0..4294967295)
Gauge ::=
[APPLICATION 2]
IMPLICIT INTEGER (0..4294967295)
TimeTicks ::=
[APPLICATION 3]
IMPLICIT INTEGER (0..4294967295)
Opaque ::=
[APPLICATION 4] -- arbitrary ASN.1 value,
IMPLICIT OCTET STRING -- "double-wrapped"
END
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -7,11 +7,15 @@ ...@@ -7,11 +7,15 @@
# Distributed under the terms of the APACHE license. # Distributed under the terms of the APACHE license.
# See LICENSE.txt for more info. # See LICENSE.txt for more info.
from .base import AbstractTestBases from tangostationcontrol.integration_test.default.devices.base import AbstractTestBases
class TestDevicePDU(AbstractTestBases.TestDeviceBase): class TestDevicePDU(AbstractTestBases.TestDeviceBase):
def setUp(self): def setUp(self):
"""Intentionally recreate the device object in each test"""
super().setUp("STAT/PDU/1") super().setUp("STAT/PDU/1")
def test_device_read_all_attributes(self):
"""Mask reading attributes not possible without SNMP"""
# TODO(Corne): Unmask this test once SNMP simulator in place
Integer32, MibScalar, MibTable, MibTableRow, MibTableColumn, TimeTicks, iso, Gauge32, MibIdentifier, Bits, Counter32 = mibBuilder.importSymbols("SNMPv2-SMI", "Integer32", "MibScalar", "MibTable", "MibTableRow", "MibTableColumn", "TimeTicks", "iso", "Gauge32", "MibIdentifier", "Bits","Counter32")
ConstraintsIntersection, ConstraintsUnion, ValueSizeConstraint, SingleValueConstraint, ValueRangeConstraint = mibBuilder.importSymbols("ASN1-REFINEMENT", "ConstraintsIntersection", "ConstraintsUnion", "ValueSizeConstraint", "SingleValueConstraint", "ValueRangeConstraint")
NamedValues, = mibBuilder.importSymbols("ASN1-ENUMERATION", "NamedValues")
testNamedValue = MibScalar((9, 8, 7, 6, 5, 4, 3, 2, 1), Integer32().subtype(subtypeSpec=ConstraintsUnion(SingleValueConstraint(1, 2, 3, 4))).clone(namedValues=NamedValues(("A", 1), ("B", 2), ("C", 3), ("D", 4)))).setMaxAccess("readonly")
mibBuilder.exportSymbols("TEST-MIB", testNamedValue=testNamedValue)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment