diff --git a/CHANGES.txt b/CHANGES.txt index 3aeb7062..22aeef8e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,15 +25,17 @@ Revision 5.0.0, released 2018-10-?? just var-binds (as var-arg), the rest of the parameters packed into opaque kwargs + * The `readVars`, `readNextVars` and `writeVars` methods of MIB + instrumentation controller return immediately and deliver their + results via a call back. + * CommandResponder application passes `snmpEngine` and optionally user-supplied `cbCtx` object throughout the MIB instrumentation methods. The goal is to let MIB objects access/modify whatever custom Python objects they need while being called back. * CommandResponder refactored to facilitate asynchronous - MIB instrumentation routines. The `readVars`, `readNextVars` and - `writeVars` MIB controller methods return immediately and - deliver their results via a call back. + MIB instrumentation routines. - The high-level API (`hlapi`) extended to cover lightweight SNMP v1arch in hope to ease the use of packet-level SNMP API. diff --git a/examples/v3arch/asyncore/agent/cmdrsp/implementing-scalar-mib-objects.py b/examples/v3arch/asyncore/agent/cmdrsp/implementing-scalar-mib-objects.py index 878f97a9..783c1e6c 100644 --- a/examples/v3arch/asyncore/agent/cmdrsp/implementing-scalar-mib-objects.py +++ b/examples/v3arch/asyncore/agent/cmdrsp/implementing-scalar-mib-objects.py @@ -56,7 +56,7 @@ MibScalar, MibScalarInstance = mibBuilder.importSymbols( class MyStaticMibScalarInstance(MibScalarInstance): # noinspection PyUnusedLocal,PyUnusedLocal - def getValue(self, name, idx): + def getValue(self, name, idx, **context): return self.getSyntax().clone( 'Python %s running on a %s platform' % (sys.version, sys.platform) ) diff --git a/examples/v3arch/asyncore/proxy/command/ipv6-to-ipv4-conversion.py b/examples/v3arch/asyncore/proxy/command/ipv6-to-ipv4-conversion.py index 9e34fd0d..df712179 100644 --- a/examples/v3arch/asyncore/proxy/command/ipv6-to-ipv4-conversion.py +++ b/examples/v3arch/asyncore/proxy/command/ipv6-to-ipv4-conversion.py @@ -96,7 +96,7 @@ class CommandResponder(cmdrsp.CommandResponderBase): v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() } - pduTypes = cmdGenMap.keys() # This app will handle these PDUs + SUPPORTED_PDU_TYPES = cmdGenMap.keys() # This app will handle these PDUs # SNMP request relay def handleMgmtOperation(self, snmpEngine, stateReference, contextName, diff --git a/examples/v3arch/asyncore/proxy/command/v2c-to-v1-conversion.py b/examples/v3arch/asyncore/proxy/command/v2c-to-v1-conversion.py index 3dd64ddf..c676e9b5 100644 --- a/examples/v3arch/asyncore/proxy/command/v2c-to-v1-conversion.py +++ b/examples/v3arch/asyncore/proxy/command/v2c-to-v1-conversion.py @@ -95,7 +95,7 @@ class CommandResponder(cmdrsp.CommandResponderBase): v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() } - pduTypes = cmdGenMap.keys() # This app will handle these PDUs + SUPPORTED_PDU_TYPES = cmdGenMap.keys() # This app will handle these PDUs # SNMP request relay def handleMgmtOperation(self, snmpEngine, stateReference, contextName, diff --git a/examples/v3arch/asyncore/proxy/command/v2c-to-v3-conversion.py b/examples/v3arch/asyncore/proxy/command/v2c-to-v3-conversion.py index f5d6167e..83d4c453 100644 --- a/examples/v3arch/asyncore/proxy/command/v2c-to-v3-conversion.py +++ b/examples/v3arch/asyncore/proxy/command/v2c-to-v3-conversion.py @@ -95,7 +95,7 @@ class CommandResponder(cmdrsp.CommandResponderBase): v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() } - pduTypes = cmdGenMap.keys() # This app will handle these PDUs + SUPPORTED_PDU_TYPES = cmdGenMap.keys() # This app will handle these PDUs # SNMP request relay def handleMgmtOperation(self, snmpEngine, stateReference, contextName, diff --git a/examples/v3arch/asyncore/proxy/command/v3-to-v2c-conversion.py b/examples/v3arch/asyncore/proxy/command/v3-to-v2c-conversion.py index b5d64790..e0c1c4d6 100644 --- a/examples/v3arch/asyncore/proxy/command/v3-to-v2c-conversion.py +++ b/examples/v3arch/asyncore/proxy/command/v3-to-v2c-conversion.py @@ -98,7 +98,7 @@ class CommandResponder(cmdrsp.CommandResponderBase): v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() } - pduTypes = cmdGenMap.keys() # This app will handle these PDUs + SUPPORTED_PDU_TYPES = cmdGenMap.keys() # This app will handle these PDUs # SNMP request relay def handleMgmtOperation(self, snmpEngine, stateReference, contextName, diff --git a/pysnmp/entity/rfc3413/cmdrsp.py b/pysnmp/entity/rfc3413/cmdrsp.py index 8f6bbab5..e2d47f38 100644 --- a/pysnmp/entity/rfc3413/cmdrsp.py +++ b/pysnmp/entity/rfc3413/cmdrsp.py @@ -15,25 +15,41 @@ from pysnmp import debug # 3.2 class CommandResponderBase(object): acmID = 3 # default MIB access control method to use - pduTypes = () + SUPPORTED_PDU_TYPES = () + SMI_ERROR_MAP = { + pysnmp.smi.error.NoAccessError: 'noAccess', + pysnmp.smi.error.WrongTypeError: 'wrongType', + pysnmp.smi.error.WrongLengthError: 'wrongLength', + pysnmp.smi.error.WrongEncodingError: 'wrongEncoding', + pysnmp.smi.error.WrongValueError: 'wrongValue', + pysnmp.smi.error.NoCreationError: 'noCreation', + pysnmp.smi.error.InconsistentValueError: 'inconsistentValue', + pysnmp.smi.error.ResourceUnavailableError: 'resourceUnavailable', + pysnmp.smi.error.CommitFailedError: 'commitFailed', + pysnmp.smi.error.UndoFailedError: 'undoFailed', + pysnmp.smi.error.AuthorizationError: 'authorizationError', + pysnmp.smi.error.NotWritableError: 'notWritable', + pysnmp.smi.error.InconsistentNameError: 'inconsistentName' + } def __init__(self, snmpEngine, snmpContext, cbCtx=None): snmpEngine.msgAndPduDsp.registerContextEngineId( - snmpContext.contextEngineId, self.pduTypes, self.processPdu + snmpContext.contextEngineId, self.SUPPORTED_PDU_TYPES, self.processPdu ) self.snmpContext = snmpContext self.cbCtx = cbCtx self.__pendingReqs = {} - def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): - pass - def close(self, snmpEngine): snmpEngine.msgAndPduDsp.unregisterContextEngineId( - self.snmpContext.contextEngineId, self.pduTypes + self.snmpContext.contextEngineId, self.SUPPORTED_PDU_TYPES ) self.snmpContext = self.__pendingReqs = None + def releaseStateInformation(self, stateReference): + if stateReference in self.__pendingReqs: + del self.__pendingReqs[stateReference] + def sendVarBinds(self, snmpEngine, stateReference, errorStatus, errorIndex, varBinds): (messageProcessingModel, @@ -105,10 +121,6 @@ class CommandResponderBase(object): _setRequestType = rfc1905.SetRequestPDU.tagSet _counter64Type = rfc1902.Counter64.tagSet - def releaseStateInformation(self, stateReference): - if stateReference in self.__pendingReqs: - del self.__pendingReqs[stateReference] - def processPdu(self, snmpEngine, messageProcessingModel, securityModel, securityName, securityLevel, contextEngineId, contextName, pduVersion, PDU, maxSizeResponseScopedPDU, stateReference): @@ -140,63 +152,11 @@ class CommandResponderBase(object): # 3.2.5 varBinds = v2c.apiPDU.getVarBinds(PDU) - errorStatus, errorIndex = 'noError', 0 debug.logger & debug.flagApp and debug.logger( 'processPdu: stateReference %s, varBinds %s' % (stateReference, varBinds)) - try: - self.initiateMgmtOperation(snmpEngine, stateReference, contextName, PDU) - - # SNMPv2 SMI exceptions - except pysnmp.smi.error.GenError: - errorIndication = sys.exc_info()[1] - debug.logger & debug.flagApp and debug.logger( - 'processPdu: stateReference %s, errorIndication %s' % (stateReference, errorIndication)) - if 'oid' in errorIndication: - # Request REPORT generation - statusInformation['oid'] = errorIndication['oid'] - statusInformation['val'] = errorIndication['val'] - - # PDU-level SMI errors - except pysnmp.smi.error.NoAccessError: - errorStatus, errorIndex = 'noAccess', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.WrongTypeError: - errorStatus, errorIndex = 'wrongType', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.WrongLengthError: - errorStatus, errorIndex = 'wrongLength', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.WrongEncodingError: - errorStatus, errorIndex = 'wrongEncoding', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.WrongValueError: - errorStatus, errorIndex = 'wrongValue', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.NoCreationError: - errorStatus, errorIndex = 'noCreation', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.InconsistentValueError: - errorStatus, errorIndex = 'inconsistentValue', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.ResourceUnavailableError: - errorStatus, errorIndex = 'resourceUnavailable', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.CommitFailedError: - errorStatus, errorIndex = 'commitFailed', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.UndoFailedError: - errorStatus, errorIndex = 'undoFailed', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.AuthorizationError: - errorStatus, errorIndex = 'authorizationError', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.NotWritableError: - errorStatus, errorIndex = 'notWritable', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.InconsistentNameError: - errorStatus, errorIndex = 'inconsistentName', sys.exc_info()[1]['idx'] + 1 - except pysnmp.smi.error.SmiError: - errorStatus, errorIndex = 'genErr', len(varBinds) and 1 or 0 - except pysnmp.error.PySnmpError: - self.releaseStateInformation(stateReference) - return - else: # successful request processor must release state info - return - - self.sendVarBinds(snmpEngine, stateReference, errorStatus, - errorIndex, varBinds) - - self.releaseStateInformation(stateReference) + self.initiateMgmtOperation(snmpEngine, stateReference, contextName, PDU) @classmethod def verifyAccess(cls, viewType, varBind, **context): @@ -258,21 +218,86 @@ class CommandResponderBase(object): # This will cause MibTree to skip this OID-value raise pysnmp.smi.error.NoAccessError(name=name, idx=context.get('idx')) + def _getMgmtFun(self, contextName): + return lambda *args, **kwargs: None -class GetCommandResponder(CommandResponderBase): - pduTypes = (rfc1905.GetRequestPDU.tagSet,) + def _checkSmiErrors(self, varBinds): + errorIndication = None + errorStatus = errorIndex = 0 + + exception = None + + for idx, varBind in enumerate(varBinds): + name, value = varBind + if isinstance(value, tuple): # expect exception tuple + debug.logger & debug.flagApp and debug.logger( + '_checkSmiErrors: exception reported for OID %s exception %s' % (name, value)) + + if not exception: + exception = value + + # reset exception object + varBinds[idx] = name, v2c.null + + try: + # TODO: perhaps chain exceptions + if exception: + debug.logger & debug.flagApp and debug.logger( + '_checkSmiErrors: re-raising exception %s' % (exception,)) + raise exception[1].with_traceback(exception[2]) + + # SNMPv2 SMI exceptions + except pysnmp.smi.error.GenError: + errorIndication = sys.exc_info()[1] + debug.logger & debug.flagApp and debug.logger( + '_checkSmiErrors: errorIndication %s' % (errorIndication,)) + + except pysnmp.smi.error.SmiError: + exc_type, exc_obj, trb = sys.exc_info() + + errorStatus = self.SMI_ERROR_MAP.get(exc_type, 'genErr') + + try: + errorIndex = exc_obj['idx'] + 1 + + except IndexError: + errorIndex = len(varBinds) and 1 or 0 + + return errorIndication, errorStatus, errorIndex def completeMgmtOperation(self, varBinds, **context): - self.sendVarBinds(context['snmpEngine'], context['stateReference'], - 0, 0, varBinds) - self.releaseStateInformation(context['stateReference']) - # rfc1905: 4.2.1 + try: + (errorIndication, + errorStatus, errorIndex) = self._checkSmiErrors(varBinds) + + except pysnmp.error.PySnmpError: + self.releaseStateInformation(context['stateReference']) + return + + stateReference = context['stateReference'] + + if errorIndication: + statusInformation = self.__pendingReqs[stateReference]['statusInformation'] + + try: + # Request REPORT generation + statusInformation['oid'] = errorIndication['oid'] + statusInformation['val'] = errorIndication['val'] + + except KeyError: + pass + + self.sendVarBinds(context['snmpEngine'], stateReference, + errorStatus, errorIndex, varBinds) + + self.releaseStateInformation(stateReference) + def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): - # rfc1905: 4.2.1.1 - mgmtFun = self.snmpContext.getMibInstrum(contextName).readVars varBinds = v2c.apiPDU.getVarBinds(PDU) + mgmtFun = self._getMgmtFun(contextName) + context = dict(snmpEngine=snmpEngine, stateReference=stateReference, acFun=self.verifyAccess, @@ -282,56 +307,54 @@ class GetCommandResponder(CommandResponderBase): mgmtFun(*varBinds, **context) +class GetCommandResponder(CommandResponderBase): + SUPPORTED_PDU_TYPES = (rfc1905.GetRequestPDU.tagSet,) + + # rfc1905: 4.2.1 + def _getMgmtFun(self, contextName): + return self.snmpContext.getMibInstrum(contextName).readVars + + class NextCommandResponder(CommandResponderBase): - pduTypes = (rfc1905.GetNextRequestPDU.tagSet,) - - def completeMgmtOperation(self, varBinds, **context): - self.sendVarBinds(context['snmpEngine'], context['stateReference'], - 0, 0, varBinds) - self.releaseStateInformation(context['stateReference']) + SUPPORTED_PDU_TYPES = (rfc1905.GetNextRequestPDU.tagSet,) # rfc1905: 4.2.2 - def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): - # rfc1905: 4.2.2.1 - mgmtFun = self.snmpContext.getMibInstrum(contextName).readNextVars - - varBinds = v2c.apiPDU.getVarBinds(PDU) - - context = dict(snmpEngine=snmpEngine, - stateReference=stateReference, - acFun=self.verifyAccess, - cbFun=self.completeMgmtOperation, - cbCtx=self.cbCtx) - - mgmtFun(*varBinds, **context) + def _getMgmtFun(self, contextName): + return self.snmpContext.getMibInstrum(contextName).readNextVars class BulkCommandResponder(CommandResponderBase): - pduTypes = (rfc1905.GetBulkRequestPDU.tagSet,) + SUPPORTED_PDU_TYPES = (rfc1905.GetBulkRequestPDU.tagSet,) maxVarBinds = 64 + def _getMgmtFun(self, contextName): + return self.snmpContext.getMibInstrum(contextName).readNextVars + def _completeNonRepeaters(self, varBinds, **context): context['rspVarBinds'][:] = varBinds - context['cbFun'] = self.completeMgmtOperation - mgmtFun = self.snmpContext.getMibInstrum(context['contextName']).readNextVars + if context['counters']['M'] and context['counters']['R']: + context['cbFun'] = self.completeMgmtOperation - mgmtFun(*context['varBinds'], **context) + mgmtFun = self._getMgmtFun(context['contextName']) + + mgmtFun(*context['reqVarBinds'], **context) + + else: + CommandResponderBase.completeMgmtOperation(self, context['rspVarBinds'], **context) def completeMgmtOperation(self, varBinds, **context): context['rspVarBinds'].extend(varBinds) context['counters']['M'] -= 1 if context['counters']['M'] and context['counters']['R']: - mgmtFun = self.snmpContext.getMibInstrum(context['contextName']).readNextVars + mgmtFun = self._getMgmtFun(context['contextName']) context['cbFun'] = self.completeMgmtOperation mgmtFun(*varBinds[-context['counters']['R']:], **context) else: - self.sendVarBinds(context['snmpEngine'], context['stateReference'], - 0, 0, varBinds) - self.releaseStateInformation(context['stateReference']) + CommandResponderBase.completeMgmtOperation(self, context['rspVarBinds'], **context) # rfc1905: 4.2.3 def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): @@ -355,7 +378,7 @@ class BulkCommandResponder(CommandResponderBase): debug.logger & debug.flagApp and debug.logger( 'initiateMgmtOperation: N %d, M %d, R %d' % (N, M, R)) - mgmtFun = self.snmpContext.getMibInstrum(contextName).readNextVars + mgmtFun = self._getMgmtFun(contextName) context = dict(snmpEngine=snmpEngine, stateReference=stateReference, @@ -363,7 +386,7 @@ class BulkCommandResponder(CommandResponderBase): acFun=self.verifyAccess, cbFun=self._completeNonRepeaters, cbCtx=self.cbCtx, - varBinds=varBinds[-R:], + reqVarBinds=varBinds[N:], counters={'M': M, 'R': R}, rspVarBinds=[]) @@ -371,32 +394,14 @@ class BulkCommandResponder(CommandResponderBase): class SetCommandResponder(CommandResponderBase): - pduTypes = (rfc1905.SetRequestPDU.tagSet,) + SUPPORTED_PDU_TYPES = (rfc1905.SetRequestPDU.tagSet,) - def completeMgmtOperation(self, varBinds, **context): - self.sendVarBinds(context['snmpEngine'], context['stateReference'], - 0, 0, varBinds) - self.releaseStateInformation(context['stateReference']) + SMI_ERROR_MAP = CommandResponderBase.SMI_ERROR_MAP.copy() - # rfc1905: 4.2.5 - def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): - mgmtFun = self.snmpContext.getMibInstrum(contextName).writeVars + # turn missing OIDs into access denial + SMI_ERROR_MAP[pysnmp.smi.error.NoSuchObjectError] = 'notWritable' + SMI_ERROR_MAP[pysnmp.smi.error.NoSuchInstanceError] = 'notWritable' - varBinds = v2c.apiPDU.getVarBinds(PDU) - - context = dict(snmpEngine=snmpEngine, - stateReference=stateReference, - acFun=self.verifyAccess, - cbFun=self.completeMgmtOperation, - cbCtx=self.cbCtx) - - # rfc1905: 4.2.5.1-13 - try: - mgmtFun(*varBinds, **context) - - except (pysnmp.smi.error.NoSuchObjectError, - pysnmp.smi.error.NoSuchInstanceError): - instrumError = pysnmp.smi.error.NotWritableError() - instrumError.update(sys.exc_info()[1]) - self.releaseStateInformation(stateReference) - raise instrumError + # rfc1905: 4.2.5.1-13 + def _getMgmtFun(self, contextName): + return self.snmpContext.getMibInstrum(contextName).writeVars diff --git a/pysnmp/entity/rfc3413/ntfrcv.py b/pysnmp/entity/rfc3413/ntfrcv.py index 34192ad3..df394a0c 100644 --- a/pysnmp/entity/rfc3413/ntfrcv.py +++ b/pysnmp/entity/rfc3413/ntfrcv.py @@ -14,12 +14,12 @@ from pysnmp import debug # 3.4 class NotificationReceiver(object): - pduTypes = (v1.TrapPDU.tagSet, v2c.SNMPv2TrapPDU.tagSet, - v2c.InformRequestPDU.tagSet) + SUPPORTED_PDU_TYPES = (v1.TrapPDU.tagSet, v2c.SNMPv2TrapPDU.tagSet, + v2c.InformRequestPDU.tagSet) def __init__(self, snmpEngine, cbFun, cbCtx=None): snmpEngine.msgAndPduDsp.registerContextEngineId( - null, self.pduTypes, self.processPdu # '' is a wildcard + null, self.SUPPORTED_PDU_TYPES, self.processPdu # '' is a wildcard ) self.__snmpTrapCommunity = '' @@ -33,7 +33,7 @@ class NotificationReceiver(object): def close(self, snmpEngine): snmpEngine.msgAndPduDsp.unregisterContextEngineId( - null, self.pduTypes + null, self.SUPPORTED_PDU_TYPES ) self.__cbFun = self.__cbCtx = None diff --git a/pysnmp/smi/instrum.py b/pysnmp/smi/instrum.py index d84493ac..cec737d6 100644 --- a/pysnmp/smi/instrum.py +++ b/pysnmp/smi/instrum.py @@ -6,6 +6,8 @@ # import sys import traceback +import functools +from pysnmp import nextid from pysnmp.smi import error from pysnmp import debug @@ -24,39 +26,59 @@ class AbstractMibInstrumController(object): class MibInstrumController(AbstractMibInstrumController): + STATUS_OK = 'ok' + STATUS_ERROR = 'err' + + STATE_START = 'start' + STATE_STOP = 'stop' + STATE_ANY = '*' + # These states are actually methods of the MIB objects + STATE_READ_TEST = 'readTest' + STATE_READ_GET = 'readGet' + STATE_READ_TEST_NEXT = 'readTestNext' + STATE_READ_GET_NEXT = 'readGetNext' + STATE_WRITE_TEST = 'writeTest' + STATE_WRITE_COMMIT = 'writeCommit' + STATE_WRITE_CLEANUP = 'writeCleanup' + STATE_WRITE_UNDO = 'writeUndo' + fsmReadVar = { # ( state, status ) -> newState - ('start', 'ok'): 'readTest', - ('readTest', 'ok'): 'readGet', - ('readGet', 'ok'): 'stop', - ('*', 'err'): 'stop' + (STATE_START, STATUS_OK): STATE_READ_TEST, + (STATE_READ_TEST, STATUS_OK): STATE_READ_GET, + (STATE_READ_GET, STATUS_OK): STATE_STOP, + (STATE_ANY, STATUS_ERROR): STATE_STOP } fsmReadNextVar = { # ( state, status ) -> newState - ('start', 'ok'): 'readTestNext', - ('readTestNext', 'ok'): 'readGetNext', - ('readGetNext', 'ok'): 'stop', - ('*', 'err'): 'stop' + (STATE_START, STATUS_OK): STATE_READ_TEST_NEXT, + (STATE_READ_TEST_NEXT, STATUS_OK): STATE_READ_GET_NEXT, + (STATE_READ_GET_NEXT, STATUS_OK): STATE_STOP, + (STATE_ANY, STATUS_ERROR): STATE_STOP } fsmWriteVar = { # ( state, status ) -> newState - ('start', 'ok'): 'writeTest', - ('writeTest', 'ok'): 'writeCommit', - ('writeCommit', 'ok'): 'writeCleanup', - ('writeCleanup', 'ok'): 'readTest', + (STATE_START, STATUS_OK): STATE_WRITE_TEST, + (STATE_WRITE_TEST, STATUS_OK): STATE_WRITE_COMMIT, + (STATE_WRITE_COMMIT, STATUS_OK): STATE_WRITE_CLEANUP, + (STATE_WRITE_CLEANUP, STATUS_OK): STATE_READ_TEST, # Do read after successful write - ('readTest', 'ok'): 'readGet', - ('readGet', 'ok'): 'stop', + (STATE_READ_TEST, STATUS_OK): STATE_READ_GET, + (STATE_READ_GET, STATUS_OK): STATE_STOP, # Error handling - ('writeTest', 'err'): 'writeCleanup', - ('writeCommit', 'err'): 'writeUndo', - ('writeUndo', 'ok'): 'readTest', + (STATE_WRITE_TEST, STATUS_ERROR): STATE_WRITE_CLEANUP, + (STATE_WRITE_COMMIT, STATUS_ERROR): STATE_WRITE_UNDO, + (STATE_WRITE_UNDO, STATUS_OK): STATE_READ_TEST, # Ignore read errors (removed columns) - ('readTest', 'err'): 'stop', - ('readGet', 'err'): 'stop', - ('*', 'err'): 'stop' + (STATE_READ_TEST, STATUS_ERROR): STATE_STOP, + (STATE_READ_GET, STATUS_ERROR): STATE_STOP, + (STATE_ANY, STATUS_ERROR): STATE_STOP } + FSM_CONTEXT = '_fsmContext' + + FSM_SESSION_ID = nextid.Integer(0xffffffff) + def __init__(self, mibBuilder): self.mibBuilder = mibBuilder self.lastBuildId = -1 @@ -183,88 +205,115 @@ class MibInstrumController(AbstractMibInstrumController): # MIB instrumentation - def flipFlopFsm(self, fsmTable, *varBinds, **context): + def _flipFlopFsmCb(self, varBind, **context): + fsmContext = context[self.FSM_CONTEXT] + varBinds = fsmContext['varBinds'] + + idx = context.pop('idx') + + if idx >= 0: + fsmContext['count'] += 1 + + varBinds[idx] = varBind + + debug.logger & debug.flagIns and debug.logger( + '_flipFlopFsmCb: var-bind %d, processed %d, expected %d' % (idx, fsmContext['count'], len(varBinds))) + + if fsmContext['count'] < len(varBinds): + return + + debug.logger & debug.flagIns and debug.logger( + '_flipFlopFsmCb: finished, output %r' % (varBinds,)) + + fsmCallable = fsmContext['fsmCallable'] + + fsmCallable(**context) + + def flipFlopFsm(self, fsmTable, *varBinds, **context): try: - fsmContext = context['fsmState'] + fsmContext = context[self.FSM_CONTEXT] except KeyError: self.__indexMib() - fsmContext = context['fsmState'] = dict(varBinds=[], state='start', status='ok') + fsmContext = context[self.FSM_CONTEXT] = dict( + sessionId=self.FSM_SESSION_ID(), + varBinds=list(varBinds[:]), + fsmCallable=functools.partial(self.flipFlopFsm, fsmTable, *varBinds), + state=self.STATE_START, status=self.STATUS_OK + ) debug.logger & debug.flagIns and debug.logger('flipFlopFsm: input var-binds %r' % (varBinds,)) mibTree, = self.mibBuilder.importSymbols('SNMPv2-SMI', 'iso') - outputVarBinds = fsmContext['varBinds'] state = fsmContext['state'] status = fsmContext['status'] - origExc = origTraceback = None + debug.logger & debug.flagIns and debug.logger( + 'flipFlopFsm: current state %s, status %s' % (state, status)) + + try: + newState = fsmTable[(state, status)] + + except KeyError: + try: + newState = fsmTable[(self.STATE_ANY, status)] + + except KeyError: + raise error.SmiError('Unresolved FSM state %s, %s' % (state, status)) + + debug.logger & debug.flagIns and debug.logger( + 'flipFlopFsm: state %s status %s -> new state %s' % (state, status, newState)) + + state = newState + + if state == self.STATE_STOP: + context.pop(self.FSM_CONTEXT, None) + + cbFun = context.get('cbFun') + if cbFun: + varBinds = fsmContext['varBinds'] + cbFun(varBinds, **context) + + return + + fsmContext.update(state=state, count=0) + + # the case of no var-binds + if not varBinds: + return self._flipFlopFsmCb(None, idx=-1, **context) + + mgmtFun = getattr(mibTree, state, None) + if not mgmtFun: + raise error.SmiError( + 'Unsupported state handler %s at %s' % (state, self) + ) + + for idx, varBind in enumerate(varBinds): + try: + # TODO: managed objects to run asynchronously + #mgmtFun(varBind, idx=idx, **context) + self._flipFlopFsmCb(mgmtFun(varBind, idx=idx, **context), idx=idx, **context) + + except error.SmiError: + exc = sys.exc_info() + debug.logger & debug.flagIns and debug.logger( + 'flipFlopFsm: fun %s exception %s for %r with traceback: %s' % ( + mgmtFun, exc[0], varBind, traceback.format_exception(*exc))) + + varBind = varBind[0], exc + + fsmContext['status'] = self.STATUS_ERROR + + self._flipFlopFsmCb(varBind, idx=idx, **context) + + return - while True: - k = state, status - if k in fsmTable: - fsmState = fsmTable[k] else: - k = '*', status - if k in fsmTable: - fsmState = fsmTable[k] - else: - raise error.SmiError( - 'Unresolved FSM state %s, %s' % (state, status) - ) - debug.logger & debug.flagIns and debug.logger( - 'flipFlopFsm: state %s status %s -> fsmState %s' % (state, status, fsmState)) - state = fsmState - status = 'ok' - if state == 'stop': - break - - for idx, (name, val) in enumerate(varBinds): - mgmtFun = getattr(mibTree, state, None) - if not mgmtFun: - raise error.SmiError( - 'Unsupported state handler %s at %s' % (state, self) - ) - - context['idx'] = idx - - try: - # Convert to tuple to avoid ObjectName instantiation - # on subscription - rval = mgmtFun((tuple(name), val), **context) - - except error.SmiError: - exc_t, exc_v, exc_tb = sys.exc_info() - debug.logger & debug.flagIns and debug.logger( - 'flipFlopFsm: fun %s exception %s for %s=%r with traceback: %s' % ( - mgmtFun, exc_t, name, val, traceback.format_exception(exc_t, exc_v, exc_tb))) - if origExc is None: # Take the first exception - origExc, origTraceback = exc_v, exc_tb - status = 'err' - break - else: - debug.logger & debug.flagIns and debug.logger( - 'flipFlopFsm: fun %s succeeded for %s=%r' % (mgmtFun, name, val)) - if rval is not None: - outputVarBinds.append((rval[0], rval[1])) - - if origExc: - if sys.version_info[0] <= 2: - raise origExc - else: - try: - raise origExc.with_traceback(origTraceback) - finally: - # Break cycle between locals and traceback object - # (seems to be irrelevant on Py3 but just in case) - del origTraceback - - cbFun = context.get('cbFun') - if cbFun: - cbFun(outputVarBinds, **context) + debug.logger & debug.flagIns and debug.logger( + 'flipFlopFsm: func %s initiated for %r' % (mgmtFun, varBind)) def readVars(self, *varBinds, **context): self.flipFlopFsm(self.fsmReadVar, *varBinds, **context)