summaryrefslogtreecommitdiff
path: root/pysnmp/smi/instrum.py
blob: 342eeb6b9622c7109efdca4620c17db198a31e84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
#
# This file is part of pysnmp software.
#
# Copyright (c) 2005-2019, Ilya Etingof <etingof@gmail.com>
# License: http://snmplabs.com/pysnmp/license.html
#
from pysnmp import debug
from pysnmp.smi import error

__all__ = ['AbstractMibInstrumController', 'MibInstrumController']


class AbstractMibInstrumController(object):
    def readMibObjects(self, *varBinds, **context):
        raise error.NoSuchInstanceError(idx=0)

    def readNextMibObjects(self, *varBinds, **context):
        raise error.EndOfMibViewError(idx=0)

    def writeMibObjects(self, *varBinds, **context):
        raise error.NoSuchObjectError(idx=0)


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'

    FSM_READ_VAR = {
        # (state, status) -> newState
        (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
    }
    FSM_READ_NEXT_VAR = {
        # (state, status) -> newState
        (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
    }
    FSM_WRITE_VAR = {
        # (state, status) -> newState
        (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
        (STATE_READ_TEST, STATUS_OK): STATE_READ_GET,
        (STATE_READ_GET, STATUS_OK): STATE_STOP,
        # Error handling
        (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)
        (STATE_READ_TEST, STATUS_ERROR): STATE_STOP,
        (STATE_READ_GET, STATUS_ERROR): STATE_STOP,
        (STATE_ANY, STATUS_ERROR): STATE_STOP
    }

    def __init__(self, mibBuilder):
        self.mibBuilder = mibBuilder
        self.lastBuildId = -1
        self.lastBuildSyms = {}

    def getMibBuilder(self):
        return self.mibBuilder

    def _indexMib(self):
        """Rebuild a tree from MIB objects found at currently loaded modules.

        If currently existing tree is out of date, walk over all Managed Objects
        and Instances to structure Management Instrumentation objects into a tree
        of the following layout:

        MibTree
          |
          +----MibScalar
          |        |
          |        +-----MibScalarInstance
          |
          +----MibTable
          |
          +----MibTableRow
                   |
                   +-------MibTableColumn
                                 |
                                 +------MibScalarInstance(s)

        Notes
        -----
        Only Managed Objects (i.e. `OBJECT-TYPE`) get indexed here, various MIB
        definitions and constants can't be SNMP managed so we drop them.
        """
        if self.lastBuildId == self.mibBuilder.lastBuildId:
            return

        (MibScalarInstance, MibScalar,
         MibTableColumn, MibTableRow,
         MibTable) = self.mibBuilder.importSymbols(
            'SNMPv2-SMI', 'MibScalarInstance', 'MibScalar',
            'MibTableColumn', 'MibTableRow', 'MibTable'
        )

        mibTree, = self.mibBuilder.importSymbols('SNMPv2-SMI', 'iso')

        scalars = {}
        instances = {}
        tables = {}
        rows = {}
        cols = {}

        # Sort by module name to give user a chance to slip-in
        # custom MIB modules (that would be sorted out first)
        mibSymbols = list(self.mibBuilder.mibSymbols.items())
        mibSymbols.sort(key=lambda x: x[0], reverse=True)

        for modName, mibMod in mibSymbols:

            for symObj in mibMod.values():

                if isinstance(symObj, MibTable):
                    tables[symObj.name] = symObj

                elif isinstance(symObj, MibTableRow):
                    rows[symObj.name] = symObj

                elif isinstance(symObj, MibTableColumn):
                    cols[symObj.name] = symObj

                elif isinstance(symObj, MibScalarInstance):
                    instances[symObj.name] = symObj

                elif isinstance(symObj, MibScalar):
                    scalars[symObj.name] = symObj

        # Detach items from each other
        for symName, parentName in self.lastBuildSyms.items():

            if parentName in scalars:
                scalars[parentName].unregisterSubtrees(symName)

            elif parentName in cols:
                cols[parentName].unregisterSubtrees(symName)

            elif parentName in rows:
                rows[parentName].unregisterSubtrees(symName)

            else:
                mibTree.unregisterSubtrees(symName)

        lastBuildSyms = {}

        # Attach Managed Objects Instances to Managed Objects
        for inst in instances.values():
            if inst.typeName in scalars:
                scalars[inst.typeName].registerSubtrees(inst)

            elif inst.typeName in cols:
                cols[inst.typeName].registerSubtrees(inst)

            else:
                raise error.SmiError(
                    'Orphan MIB scalar instance %r at '
                    '%r' % (inst, self))

            lastBuildSyms[inst.name] = inst.typeName

        # Attach Table Columns to Table Rows
        for col in cols.values():
            rowName = col.name[:-1]  # XXX

            if rowName in rows:
                rows[rowName].registerSubtrees(col)

            else:
                raise error.SmiError(
                    'Orphan MIB table column %r at '
                    '%r' % (col, self))

            lastBuildSyms[col.name] = rowName

        # Attach Table Rows to MIB tree
        for row in rows.values():
            mibTree.registerSubtrees(row)
            lastBuildSyms[row.name] = mibTree.name

        # Attach Tables to MIB tree
        for table in tables.values():
            mibTree.registerSubtrees(table)
            lastBuildSyms[table.name] = mibTree.name

        # Attach Scalars to MIB tree
        for scalar in scalars.values():
            mibTree.registerSubtrees(scalar)
            lastBuildSyms[scalar.name] = mibTree.name

        self.lastBuildSyms = lastBuildSyms

        self.lastBuildId = self.mibBuilder.lastBuildId

        debug.logger & debug.FLAG_INS and debug.logger('_indexMib: rebuilt')

    def flipFlopFsm(self, fsmTable, *varBinds, **context):
        """Read, modify, create or remove Managed Objects Instances.

        Given one or more py:class:`~pysnmp.smi.rfc1902.ObjectType`, recursively
        transitions corresponding Managed Objects Instances through the Finite State
        Machine (FSM) states till it reaches its final stop state.

        Parameters
        ----------
        fsmTable: :py:class:`dict`
            A map of (`state`, `status`) -> `state` representing FSM transition matrix.
            See :py:class:`RowStatus` for FSM transition logic.

        varBinds: :py:class:`tuple` of :py:class:`~pysnmp.smi.rfc1902.ObjectType` objects
            representing Managed Objects Instances to work with.

        Other Parameters
        ----------------
        \*\*context:

            Query parameters:

            * `cbFun` (callable) - user-supplied callable that is invoked to
                pass the new value of the Managed Object Instance or an error.

            * `acFun` (callable) - user-supplied callable that is invoked to
                authorize access to the requested Managed Object Instance. If
                not supplied, no access control will be performed.

        Notes
        -----
        The callback functions (e.g. `cbFun`, `acFun`) have the same signature
        as this method where `varBind` contains the new Managed Object Instance
        value.

        In case of errors, the `errors` key in the `context` dict will contain
        a sequence of `dict` objects describing one or more errors that occur.

        Such error `dict` will have the `error`, `idx` and `state` keys providing
        the details concerning the error, for which variable-binding and in what
        state the system has failed.
        """
        count = [0]

        cbFun = context.get('cbFun')

        def _cbFun(varBind, **context):
            idx = context.pop('idx', None)

            err = context.pop('error', None)
            if err:
                # Move other errors into the errors sequence
                errors = context['errors']

                errors.append(
                    {'error': err,
                     'idx': idx,
                     'varbind': varBind,
                     'state': context['state']})

                context['status'] = self.STATUS_ERROR

            if idx is None:
                if cbFun:
                    cbFun((), **context)
                return

            _varBinds = context['varBinds']

            _varBinds[idx] = varBind

            count[0] += 1

            debug.logger & debug.FLAG_INS and debug.logger(
                '_cbFun: var-bind %d, processed %d, expected '
                '%d' % (idx, count[0], len(varBinds)))

            if count[0] < len(varBinds):
                return

            debug.logger & debug.FLAG_INS and debug.logger(
                '_cbFun: finished, output var-binds %r' % (_varBinds,))

            self.flipFlopFsm(fsmTable, *varBinds, **dict(context, cbFun=cbFun))

        debug.logger & debug.FLAG_INS and debug.logger(
            'flipFlopFsm: input var-binds %r' % (varBinds,))

        mibTree, = self.mibBuilder.importSymbols('SNMPv2-SMI', 'iso')

        try:
            state = context['state']
            status = context['status']
            instances = context['instances']
            errors = context['errors']
            _varBinds = context['varBinds']

        except KeyError:
            state, status = self.STATE_START, self.STATUS_OK
            instances = {}
            errors = []
            _varBinds = list(varBinds)

            self._indexMib()

        debug.logger & debug.FLAG_INS 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.FLAG_INS and debug.logger(
            'flipFlopFsm: state %s status %s -> transitioned into state '
            '%s' % (state, status, newState))

        state = newState

        if state == self.STATE_STOP:
            context.pop('state', None)
            context.pop('status', None)
            context.pop('instances', None)
            context.pop('varBinds', None)

            if cbFun:
                cbFun(_varBinds, **context)

            return

        # the case of no var-binds
        if not varBinds:
            _cbFun(None, **context)
            return

        actionFun = getattr(mibTree, state, None)
        if not actionFun:
            raise error.SmiError(
                'Unsupported state handler %s at '
                '%s' % (state, self))

        for idx, varBind in enumerate(varBinds):
            actionFun(
                varBind,
                **dict(context, cbFun=_cbFun, state=state, status=status,
                       idx=idx, total=len(varBinds), instances=instances,
                       errors=errors, varBinds=_varBinds, nextName=None))

            debug.logger & debug.FLAG_INS and debug.logger(
                'flipFlopFsm: func %s initiated for %r' % (actionFun, varBind))

    @staticmethod
    def _defaultErrorHandler(varBinds, **context):
        """Raise exception on any error if user callback is missing"""
        errors = context.get('errors')

        if errors:
            err = errors[-1]
            raise err['error']

    def readMibObjects(self, *varBinds, **context):
        """Read Managed Objects Instances.

        Given one or more py:class:`~pysnmp.smi.rfc1902.ObjectType` objects, read
        all or none of the referenced Managed Objects Instances.

        Parameters
        ----------
        varBinds: :py:class:`tuple` of :py:class:`~pysnmp.smi.rfc1902.ObjectType` objects
            representing Managed Objects Instances to read.

        Other Parameters
        ----------------
        \*\*context:

            Query parameters:

            * `cbFun` (callable) - user-supplied callable that is invoked to
                pass the new value of the Managed Object Instance or an error.
                If not provided, default function will raise exception in case
                of an error.

            * `acFun` (callable) - user-supplied callable that is invoked to
                authorize access to the requested Managed Object Instance. If
                not supplied, no access control will be performed.

        Notes
        -----
        The signature of the callback functions (e.g. `cbFun`, `acFun`) is this:

        .. code-block: python

            def cbFun(varBinds, **context):
                errors = context.get(errors)
                if errors:
                    print(errors[0].error)

                else:
                    print(', '.join('%s = %s' % varBind for varBind in varBinds))

        In case of errors, the `errors` key in the `context` dict will contain
        a sequence of `dict` objects describing one or more errors that occur.

        If a non-existing Managed Object is referenced, no error will be
        reported, but the values returned in the `varBinds` would be either
        :py:class:`NoSuchObject` (indicating non-existent Managed Object) or
        :py:class:`NoSuchInstance` (if Managed Object exists, but is not
        instantiated).
        """
        if 'cbFun' not in context:
            context['cbFun'] = self._defaultErrorHandler

        self.flipFlopFsm(self.FSM_READ_VAR, *varBinds, **context)

    def readNextMibObjects(self, *varBinds, **context):
        """Read Managed Objects Instances next to the given ones.

        Given one or more py:class:`~pysnmp.smi.rfc1902.ObjectType` objects, read
        all or none of the Managed Objects Instances next to the referenced ones.

        Parameters
        ----------
        varBinds: :py:class:`tuple` of :py:class:`~pysnmp.smi.rfc1902.ObjectType` objects
            representing Managed Objects Instances to read next to.

        Other Parameters
        ----------------
        \*\*context:

            Query parameters:

            * `cbFun` (callable) - user-supplied callable that is invoked to
                pass the new value of the Managed Object Instance or an error.
                If not provided, default function will raise exception in case
                of an error.

            * `acFun` (callable) - user-supplied callable that is invoked to
                authorize access to the requested Managed Object Instance. If
                not supplied, no access control will be performed.

        Notes
        -----
        The signature of the callback functions (e.g. `cbFun`, `acFun`) is this:

        .. code-block: python

            def cbFun(varBinds, **context):
                errors = context.get(errors)
                if errors:
                    print(errors[0].error)

                else:
                    print(', '.join('%s = %s' % varBind for varBind in varBinds))

        In case of errors, the `errors` key in the `context` dict will contain
        a sequence of `dict` objects describing one or more errors that occur.

        If a non-existing Managed Object is referenced, no error will be
        reported, but the values returned in the `varBinds` would be one of:
        :py:class:`NoSuchObject` (indicating non-existent Managed Object) or
        :py:class:`NoSuchInstance` (if Managed Object exists, but is not
        instantiated) or :py:class:`EndOfMibView` (when the last Managed Object
        Instance has been read).

        When :py:class:`NoSuchObject` or :py:class:`NoSuchInstance` values are
        returned, the caller is expected to repeat the same call with some
        or all `varBinds` returned to progress towards the end of the
        implemented MIB.
        """
        if 'cbFun' not in context:
            context['cbFun'] = self._defaultErrorHandler

        self.flipFlopFsm(self.FSM_READ_NEXT_VAR, *varBinds, **context)

    def writeMibObjects(self, *varBinds, **context):
        """Create, destroy or modify Managed Objects Instances.

        Given one or more py:class:`~pysnmp.smi.rfc1902.ObjectType` objects, create,
        destroy or modify  all or none of the referenced Managed Objects Instances.

        If a non-existing Managed Object Instance is written, the new Managed Object
        Instance will be created with the value given in the `varBinds`.

        If existing Managed Object Instance is being written, its value is changed
        to the new one.

        Unless it's a :py:class:`RowStatus` object of a SMI table, in which case the
        outcome of the *write* operation depends on the :py:class:`RowStatus`
        transition. The whole table row could be created or destroyed or brought
        on/offline.

        When SMI table row is brought online (i.e. into the *active* state), all
        columns will be checked for consistency. Error will be reported and write
        operation will fail if inconsistency is found.

        Parameters
        ----------
        varBinds: :py:class:`tuple` of :py:class:`~pysnmp.smi.rfc1902.ObjectType` objects
            representing Managed Objects Instances to modify.

        Other Parameters
        ----------------
        \*\*context:

            Query parameters:

            * `cbFun` (callable) - user-supplied callable that is invoked to
                pass the new value of the Managed Object Instance or an error.
                If not provided, default function will raise exception in case
                of an error.

            * `acFun` (callable) - user-supplied callable that is invoked to
                authorize access to the requested Managed Object Instance. If
                not supplied, no access control will be performed.

        Notes
        -----
        The signature of the callback functions (e.g. `cbFun`, `acFun`) is this:

        .. code-block: python

            def cbFun(varBinds, **context):
                errors = context.get(errors)
                if errors:
                    print(errors[0].error)

                else:
                    print(', '.join('%s = %s' % varBind for varBind in varBinds))

        In case of errors, the `errors` key in the `context` dict will contain
        a sequence of `dict` objects describing one or more errors that occur.

        If a non-existing Managed Object is referenced, no error will be
        reported, but the values returned in the `varBinds` would be one of:
        :py:class:`NoSuchObject` (indicating non-existent Managed Object) or
        :py:class:`NoSuchInstance` (if Managed Object exists, but can't be
        modified.
        """
        if 'cbFun' not in context:
            context['cbFun'] = self._defaultErrorHandler

        self.flipFlopFsm(self.FSM_WRITE_VAR, *varBinds, **context)