Refactor MIB state machine into asynchronous operations (#210)

Convert to async MIB instrumentation API (#210)

MIB instrumentation API changed to allow for asynchronous
managed objects access. The MIB instrumentation methods
called by the state machine now return immediately and
resume once the callback is called.

The built-in SNMPv2-SMI objects are still synchronous.

This change is a prerequisite for fully asynchronous managed objects
implementation.
async-managed-objects
Ilya Etingof 2018-10-24 10:14:33 +02:00 committed by GitHub
parent 534a5bb810
commit 0c0d054e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 280 additions and 224 deletions

View File

@ -25,15 +25,17 @@ Revision 5.0.0, released 2018-10-??
just var-binds (as var-arg), the rest of the parameters packed just var-binds (as var-arg), the rest of the parameters packed
into opaque kwargs 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 * CommandResponder application passes `snmpEngine` and optionally
user-supplied `cbCtx` object throughout the MIB instrumentation user-supplied `cbCtx` object throughout the MIB instrumentation
methods. The goal is to let MIB objects access/modify whatever methods. The goal is to let MIB objects access/modify whatever
custom Python objects they need while being called back. custom Python objects they need while being called back.
* CommandResponder refactored to facilitate asynchronous * CommandResponder refactored to facilitate asynchronous
MIB instrumentation routines. The `readVars`, `readNextVars` and MIB instrumentation routines.
`writeVars` MIB controller methods return immediately and
deliver their results via a call back.
- The high-level API (`hlapi`) extended to cover lightweight SNMP v1arch - The high-level API (`hlapi`) extended to cover lightweight SNMP v1arch
in hope to ease the use of packet-level SNMP API. in hope to ease the use of packet-level SNMP API.

View File

@ -56,7 +56,7 @@ MibScalar, MibScalarInstance = mibBuilder.importSymbols(
class MyStaticMibScalarInstance(MibScalarInstance): class MyStaticMibScalarInstance(MibScalarInstance):
# noinspection PyUnusedLocal,PyUnusedLocal # noinspection PyUnusedLocal,PyUnusedLocal
def getValue(self, name, idx): def getValue(self, name, idx, **context):
return self.getSyntax().clone( return self.getSyntax().clone(
'Python %s running on a %s platform' % (sys.version, sys.platform) 'Python %s running on a %s platform' % (sys.version, sys.platform)
) )

View File

@ -96,7 +96,7 @@ class CommandResponder(cmdrsp.CommandResponderBase):
v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(),
v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() 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 # SNMP request relay
def handleMgmtOperation(self, snmpEngine, stateReference, contextName, def handleMgmtOperation(self, snmpEngine, stateReference, contextName,

View File

@ -95,7 +95,7 @@ class CommandResponder(cmdrsp.CommandResponderBase):
v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(),
v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() 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 # SNMP request relay
def handleMgmtOperation(self, snmpEngine, stateReference, contextName, def handleMgmtOperation(self, snmpEngine, stateReference, contextName,

View File

@ -95,7 +95,7 @@ class CommandResponder(cmdrsp.CommandResponderBase):
v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(),
v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() 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 # SNMP request relay
def handleMgmtOperation(self, snmpEngine, stateReference, contextName, def handleMgmtOperation(self, snmpEngine, stateReference, contextName,

View File

@ -98,7 +98,7 @@ class CommandResponder(cmdrsp.CommandResponderBase):
v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(), v2c.GetNextRequestPDU.tagSet: cmdgen.NextCommandGeneratorSingleRun(),
v2c.GetBulkRequestPDU.tagSet: cmdgen.BulkCommandGeneratorSingleRun() 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 # SNMP request relay
def handleMgmtOperation(self, snmpEngine, stateReference, contextName, def handleMgmtOperation(self, snmpEngine, stateReference, contextName,

View File

@ -15,25 +15,41 @@ from pysnmp import debug
# 3.2 # 3.2
class CommandResponderBase(object): class CommandResponderBase(object):
acmID = 3 # default MIB access control method to use 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): def __init__(self, snmpEngine, snmpContext, cbCtx=None):
snmpEngine.msgAndPduDsp.registerContextEngineId( snmpEngine.msgAndPduDsp.registerContextEngineId(
snmpContext.contextEngineId, self.pduTypes, self.processPdu snmpContext.contextEngineId, self.SUPPORTED_PDU_TYPES, self.processPdu
) )
self.snmpContext = snmpContext self.snmpContext = snmpContext
self.cbCtx = cbCtx self.cbCtx = cbCtx
self.__pendingReqs = {} self.__pendingReqs = {}
def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU):
pass
def close(self, snmpEngine): def close(self, snmpEngine):
snmpEngine.msgAndPduDsp.unregisterContextEngineId( snmpEngine.msgAndPduDsp.unregisterContextEngineId(
self.snmpContext.contextEngineId, self.pduTypes self.snmpContext.contextEngineId, self.SUPPORTED_PDU_TYPES
) )
self.snmpContext = self.__pendingReqs = None self.snmpContext = self.__pendingReqs = None
def releaseStateInformation(self, stateReference):
if stateReference in self.__pendingReqs:
del self.__pendingReqs[stateReference]
def sendVarBinds(self, snmpEngine, stateReference, def sendVarBinds(self, snmpEngine, stateReference,
errorStatus, errorIndex, varBinds): errorStatus, errorIndex, varBinds):
(messageProcessingModel, (messageProcessingModel,
@ -105,10 +121,6 @@ class CommandResponderBase(object):
_setRequestType = rfc1905.SetRequestPDU.tagSet _setRequestType = rfc1905.SetRequestPDU.tagSet
_counter64Type = rfc1902.Counter64.tagSet _counter64Type = rfc1902.Counter64.tagSet
def releaseStateInformation(self, stateReference):
if stateReference in self.__pendingReqs:
del self.__pendingReqs[stateReference]
def processPdu(self, snmpEngine, messageProcessingModel, securityModel, def processPdu(self, snmpEngine, messageProcessingModel, securityModel,
securityName, securityLevel, contextEngineId, contextName, securityName, securityLevel, contextEngineId, contextName,
pduVersion, PDU, maxSizeResponseScopedPDU, stateReference): pduVersion, PDU, maxSizeResponseScopedPDU, stateReference):
@ -140,63 +152,11 @@ class CommandResponderBase(object):
# 3.2.5 # 3.2.5
varBinds = v2c.apiPDU.getVarBinds(PDU) varBinds = v2c.apiPDU.getVarBinds(PDU)
errorStatus, errorIndex = 'noError', 0
debug.logger & debug.flagApp and debug.logger( debug.logger & debug.flagApp and debug.logger(
'processPdu: stateReference %s, varBinds %s' % (stateReference, varBinds)) 'processPdu: stateReference %s, varBinds %s' % (stateReference, varBinds))
try: self.initiateMgmtOperation(snmpEngine, stateReference, contextName, PDU)
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)
@classmethod @classmethod
def verifyAccess(cls, viewType, varBind, **context): def verifyAccess(cls, viewType, varBind, **context):
@ -258,21 +218,86 @@ class CommandResponderBase(object):
# This will cause MibTree to skip this OID-value # This will cause MibTree to skip this OID-value
raise pysnmp.smi.error.NoAccessError(name=name, idx=context.get('idx')) raise pysnmp.smi.error.NoAccessError(name=name, idx=context.get('idx'))
def _getMgmtFun(self, contextName):
return lambda *args, **kwargs: None
class GetCommandResponder(CommandResponderBase): def _checkSmiErrors(self, varBinds):
pduTypes = (rfc1905.GetRequestPDU.tagSet,) 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): 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): def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU):
# rfc1905: 4.2.1.1
mgmtFun = self.snmpContext.getMibInstrum(contextName).readVars
varBinds = v2c.apiPDU.getVarBinds(PDU) varBinds = v2c.apiPDU.getVarBinds(PDU)
mgmtFun = self._getMgmtFun(contextName)
context = dict(snmpEngine=snmpEngine, context = dict(snmpEngine=snmpEngine,
stateReference=stateReference, stateReference=stateReference,
acFun=self.verifyAccess, acFun=self.verifyAccess,
@ -282,56 +307,54 @@ class GetCommandResponder(CommandResponderBase):
mgmtFun(*varBinds, **context) 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): class NextCommandResponder(CommandResponderBase):
pduTypes = (rfc1905.GetNextRequestPDU.tagSet,) SUPPORTED_PDU_TYPES = (rfc1905.GetNextRequestPDU.tagSet,)
def completeMgmtOperation(self, varBinds, **context):
self.sendVarBinds(context['snmpEngine'], context['stateReference'],
0, 0, varBinds)
self.releaseStateInformation(context['stateReference'])
# rfc1905: 4.2.2 # rfc1905: 4.2.2
def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): def _getMgmtFun(self, contextName):
# rfc1905: 4.2.2.1 return self.snmpContext.getMibInstrum(contextName).readNextVars
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)
class BulkCommandResponder(CommandResponderBase): class BulkCommandResponder(CommandResponderBase):
pduTypes = (rfc1905.GetBulkRequestPDU.tagSet,) SUPPORTED_PDU_TYPES = (rfc1905.GetBulkRequestPDU.tagSet,)
maxVarBinds = 64 maxVarBinds = 64
def _getMgmtFun(self, contextName):
return self.snmpContext.getMibInstrum(contextName).readNextVars
def _completeNonRepeaters(self, varBinds, **context): def _completeNonRepeaters(self, varBinds, **context):
context['rspVarBinds'][:] = varBinds 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): def completeMgmtOperation(self, varBinds, **context):
context['rspVarBinds'].extend(varBinds) context['rspVarBinds'].extend(varBinds)
context['counters']['M'] -= 1 context['counters']['M'] -= 1
if context['counters']['M'] and context['counters']['R']: if context['counters']['M'] and context['counters']['R']:
mgmtFun = self.snmpContext.getMibInstrum(context['contextName']).readNextVars mgmtFun = self._getMgmtFun(context['contextName'])
context['cbFun'] = self.completeMgmtOperation context['cbFun'] = self.completeMgmtOperation
mgmtFun(*varBinds[-context['counters']['R']:], **context) mgmtFun(*varBinds[-context['counters']['R']:], **context)
else: else:
self.sendVarBinds(context['snmpEngine'], context['stateReference'], CommandResponderBase.completeMgmtOperation(self, context['rspVarBinds'], **context)
0, 0, varBinds)
self.releaseStateInformation(context['stateReference'])
# rfc1905: 4.2.3 # rfc1905: 4.2.3
def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU):
@ -355,7 +378,7 @@ class BulkCommandResponder(CommandResponderBase):
debug.logger & debug.flagApp and debug.logger( debug.logger & debug.flagApp and debug.logger(
'initiateMgmtOperation: N %d, M %d, R %d' % (N, M, R)) 'initiateMgmtOperation: N %d, M %d, R %d' % (N, M, R))
mgmtFun = self.snmpContext.getMibInstrum(contextName).readNextVars mgmtFun = self._getMgmtFun(contextName)
context = dict(snmpEngine=snmpEngine, context = dict(snmpEngine=snmpEngine,
stateReference=stateReference, stateReference=stateReference,
@ -363,7 +386,7 @@ class BulkCommandResponder(CommandResponderBase):
acFun=self.verifyAccess, acFun=self.verifyAccess,
cbFun=self._completeNonRepeaters, cbFun=self._completeNonRepeaters,
cbCtx=self.cbCtx, cbCtx=self.cbCtx,
varBinds=varBinds[-R:], reqVarBinds=varBinds[N:],
counters={'M': M, 'R': R}, counters={'M': M, 'R': R},
rspVarBinds=[]) rspVarBinds=[])
@ -371,32 +394,14 @@ class BulkCommandResponder(CommandResponderBase):
class SetCommandResponder(CommandResponderBase): class SetCommandResponder(CommandResponderBase):
pduTypes = (rfc1905.SetRequestPDU.tagSet,) SUPPORTED_PDU_TYPES = (rfc1905.SetRequestPDU.tagSet,)
def completeMgmtOperation(self, varBinds, **context): SMI_ERROR_MAP = CommandResponderBase.SMI_ERROR_MAP.copy()
self.sendVarBinds(context['snmpEngine'], context['stateReference'],
0, 0, varBinds)
self.releaseStateInformation(context['stateReference'])
# rfc1905: 4.2.5 # turn missing OIDs into access denial
def initiateMgmtOperation(self, snmpEngine, stateReference, contextName, PDU): SMI_ERROR_MAP[pysnmp.smi.error.NoSuchObjectError] = 'notWritable'
mgmtFun = self.snmpContext.getMibInstrum(contextName).writeVars SMI_ERROR_MAP[pysnmp.smi.error.NoSuchInstanceError] = 'notWritable'
varBinds = v2c.apiPDU.getVarBinds(PDU) # rfc1905: 4.2.5.1-13
def _getMgmtFun(self, contextName):
context = dict(snmpEngine=snmpEngine, return self.snmpContext.getMibInstrum(contextName).writeVars
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

View File

@ -14,12 +14,12 @@ from pysnmp import debug
# 3.4 # 3.4
class NotificationReceiver(object): class NotificationReceiver(object):
pduTypes = (v1.TrapPDU.tagSet, v2c.SNMPv2TrapPDU.tagSet, SUPPORTED_PDU_TYPES = (v1.TrapPDU.tagSet, v2c.SNMPv2TrapPDU.tagSet,
v2c.InformRequestPDU.tagSet) v2c.InformRequestPDU.tagSet)
def __init__(self, snmpEngine, cbFun, cbCtx=None): def __init__(self, snmpEngine, cbFun, cbCtx=None):
snmpEngine.msgAndPduDsp.registerContextEngineId( snmpEngine.msgAndPduDsp.registerContextEngineId(
null, self.pduTypes, self.processPdu # '' is a wildcard null, self.SUPPORTED_PDU_TYPES, self.processPdu # '' is a wildcard
) )
self.__snmpTrapCommunity = '' self.__snmpTrapCommunity = ''
@ -33,7 +33,7 @@ class NotificationReceiver(object):
def close(self, snmpEngine): def close(self, snmpEngine):
snmpEngine.msgAndPduDsp.unregisterContextEngineId( snmpEngine.msgAndPduDsp.unregisterContextEngineId(
null, self.pduTypes null, self.SUPPORTED_PDU_TYPES
) )
self.__cbFun = self.__cbCtx = None self.__cbFun = self.__cbCtx = None

View File

@ -6,6 +6,8 @@
# #
import sys import sys
import traceback import traceback
import functools
from pysnmp import nextid
from pysnmp.smi import error from pysnmp.smi import error
from pysnmp import debug from pysnmp import debug
@ -24,39 +26,59 @@ class AbstractMibInstrumController(object):
class MibInstrumController(AbstractMibInstrumController): 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 = { fsmReadVar = {
# ( state, status ) -> newState # ( state, status ) -> newState
('start', 'ok'): 'readTest', (STATE_START, STATUS_OK): STATE_READ_TEST,
('readTest', 'ok'): 'readGet', (STATE_READ_TEST, STATUS_OK): STATE_READ_GET,
('readGet', 'ok'): 'stop', (STATE_READ_GET, STATUS_OK): STATE_STOP,
('*', 'err'): 'stop' (STATE_ANY, STATUS_ERROR): STATE_STOP
} }
fsmReadNextVar = { fsmReadNextVar = {
# ( state, status ) -> newState # ( state, status ) -> newState
('start', 'ok'): 'readTestNext', (STATE_START, STATUS_OK): STATE_READ_TEST_NEXT,
('readTestNext', 'ok'): 'readGetNext', (STATE_READ_TEST_NEXT, STATUS_OK): STATE_READ_GET_NEXT,
('readGetNext', 'ok'): 'stop', (STATE_READ_GET_NEXT, STATUS_OK): STATE_STOP,
('*', 'err'): 'stop' (STATE_ANY, STATUS_ERROR): STATE_STOP
} }
fsmWriteVar = { fsmWriteVar = {
# ( state, status ) -> newState # ( state, status ) -> newState
('start', 'ok'): 'writeTest', (STATE_START, STATUS_OK): STATE_WRITE_TEST,
('writeTest', 'ok'): 'writeCommit', (STATE_WRITE_TEST, STATUS_OK): STATE_WRITE_COMMIT,
('writeCommit', 'ok'): 'writeCleanup', (STATE_WRITE_COMMIT, STATUS_OK): STATE_WRITE_CLEANUP,
('writeCleanup', 'ok'): 'readTest', (STATE_WRITE_CLEANUP, STATUS_OK): STATE_READ_TEST,
# Do read after successful write # Do read after successful write
('readTest', 'ok'): 'readGet', (STATE_READ_TEST, STATUS_OK): STATE_READ_GET,
('readGet', 'ok'): 'stop', (STATE_READ_GET, STATUS_OK): STATE_STOP,
# Error handling # Error handling
('writeTest', 'err'): 'writeCleanup', (STATE_WRITE_TEST, STATUS_ERROR): STATE_WRITE_CLEANUP,
('writeCommit', 'err'): 'writeUndo', (STATE_WRITE_COMMIT, STATUS_ERROR): STATE_WRITE_UNDO,
('writeUndo', 'ok'): 'readTest', (STATE_WRITE_UNDO, STATUS_OK): STATE_READ_TEST,
# Ignore read errors (removed columns) # Ignore read errors (removed columns)
('readTest', 'err'): 'stop', (STATE_READ_TEST, STATUS_ERROR): STATE_STOP,
('readGet', 'err'): 'stop', (STATE_READ_GET, STATUS_ERROR): STATE_STOP,
('*', 'err'): 'stop' (STATE_ANY, STATUS_ERROR): STATE_STOP
} }
FSM_CONTEXT = '_fsmContext'
FSM_SESSION_ID = nextid.Integer(0xffffffff)
def __init__(self, mibBuilder): def __init__(self, mibBuilder):
self.mibBuilder = mibBuilder self.mibBuilder = mibBuilder
self.lastBuildId = -1 self.lastBuildId = -1
@ -183,88 +205,115 @@ class MibInstrumController(AbstractMibInstrumController):
# MIB instrumentation # 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: try:
fsmContext = context['fsmState'] fsmContext = context[self.FSM_CONTEXT]
except KeyError: except KeyError:
self.__indexMib() 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,)) debug.logger & debug.flagIns and debug.logger('flipFlopFsm: input var-binds %r' % (varBinds,))
mibTree, = self.mibBuilder.importSymbols('SNMPv2-SMI', 'iso') mibTree, = self.mibBuilder.importSymbols('SNMPv2-SMI', 'iso')
outputVarBinds = fsmContext['varBinds']
state = fsmContext['state'] state = fsmContext['state']
status = fsmContext['status'] 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: else:
k = '*', status debug.logger & debug.flagIns and debug.logger(
if k in fsmTable: 'flipFlopFsm: func %s initiated for %r' % (mgmtFun, varBind))
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)
def readVars(self, *varBinds, **context): def readVars(self, *varBinds, **context):
self.flipFlopFsm(self.fsmReadVar, *varBinds, **context) self.flipFlopFsm(self.fsmReadVar, *varBinds, **context)