Ignore SNMP errors from alternate MIB instances#4102
Conversation
MultiMibMixIn queries community- or context-indexed MIB instances using alternate agents. __timeout_handler only trapped TimeoutError for these non-base instances, so any other SNMP error derailed collection from all instances and aborted the whole job. Cisco FirePower appliances answer community-indexed BRIDGE-MIB queries with the SNMP error-status noAccess, which pynetsnmp surfaces as an SnmpError reading "Packet for <ip> has error: Permission denied". This aborted every topo and inventory job for the affected devices. Trap SnmpError alongside TimeoutError for non-base instances, matching the documented intent that errors for anything but the primary instance should not derail collection. Errors on the base instance still propagate. Closes Uninett#4101 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lunkwill42
left a comment
There was a problem hiding this comment.
Thanks, this seems quite reasonable — the diagnosis is spot on and the fix is the right shape. I traced the pynetsnmp error path myself and can confirm that trapping the base SnmpError for alternate instances is correct: there's no narrower class to catch for noAccess (unfortunately). The base-instance guard is also race-free, since _multiquery yields each instance sequentially, so errors on the primary instance still propagate as before.
I've left a few inline notes — the only two I'd genuinely like addressed are the method name (now a bit of a misnomer) and, if it's not too much bother, a test that drives a failure through _multiquery so we cover the wiring and not just the handler in isolation. Everything else is minor and optional.
| @@ -656,17 +657,20 @@ class constructor; result is the actual result | |||
| return integrator(results) | |||
|
|
|||
| def __timeout_handler(self, failure, descr): | |||
There was a problem hiding this comment.
Now that this also swallows access errors and not just timeouts, the name __timeout_handler undersells what it does — and it's easy to confuse with the _snmp_timeout_handler closure earlier in this same module. Could you rename it to something like __ignore_alternate_instance_error / __alternate_instance_errback? There are only three references to update: the def, the .addErrback(...) call in _multiquery, and the test.
| if self.agent_proxy is not self._base_agent: | ||
| failure.trap(TimeoutError, defer.TimeoutError) | ||
| self._logger.debug("ignoring timeout from %r", descr) | ||
| failure.trap(TimeoutError, defer.TimeoutError, SnmpError) |
There was a problem hiding this comment.
Just recording the reasoning here, since a future reader might wonder if trapping the base SnmpError is too broad — it isn't. In pynetsnmp only noAccess, resourceUnavailable and authorizationError are raised as SnmpError (distinguished only by the message string), so there is no finer-grained class to catch; genErr/noSuchName don't raise at all, they come back as empty results. One thing to be aware of: Snmpv3Error subclasses SnmpError, so a v3 auth/context failure on an alternate instance will now be swallowed too. That's consistent with how timeouts on alternates are already tolerated and only affects non-base instances, so I'm happy with it — just flagging it as a conscious choice.
|
|
||
| def _handle(self, mixin, failure, descr="vlan100"): | ||
| # __timeout_handler is name-mangled | ||
| return mixin._MultiMibMixIn__timeout_handler(failure, descr) |
There was a problem hiding this comment.
These tests call the name-mangled handler directly, which verifies its logic but not that it's actually wired up as the errback in _multiquery — if the .addErrback(self.__timeout_handler, descr) line were dropped, all three would still pass. Testing the handler in isolation is a fair tradeoff given the alternative is driving a full inlineCallbacks/reactor deferred, but it'd be nice to also have one test that pushes a failing deferred through _multiquery and asserts the job survives — that's the behaviour #4101 is really about. Not blocking.
| def test_when_base_instance_returns_snmp_error_then_it_should_propagate(self): | ||
| mixin = self._make_mixin() | ||
| # agent_proxy is left as the base agent | ||
| failure = _make_failure(SnmpError("Packet for 10.0.0.1 has error: genErr")) |
There was a problem hiding this comment.
Minor: genErr doesn't actually surface as an SnmpError in pynetsnmp (it comes back as an empty result), so this sample message is slightly misleading for a future reader. The handler ignores the message text so the test is correct either way — maybe just use a message that reflects a real trigger (Permission denied / Authorization error) or something neutral.
| from nav.mibs.types import LogicalMibInstance | ||
|
|
||
|
|
||
| def _make_failure(exception): |
There was a problem hiding this comment.
Optional nit: this wrapper doesn't buy much over calling Failure(...) directly — _make_failure(SnmpError(...)) is a touch longer than Failure(SnmpError(...)) and adds one indirection. I'd inline it and drop the helper. If you keep it, you should still adhere to the stepdown rule and place it somewhere after the higher-level code that utilizes it.
| class TestMultiMibMixInTimeoutHandler: | ||
| """Tests for MultiMibMixIn.__timeout_handler()""" | ||
|
|
||
| def _make_mixin(self): |
There was a problem hiding this comment.
House-style note: We try to follow the step-down rule in test modules too, i.e. the tests read top-to-bottom as the high-level story with helpers (_make_mixin, _handle) placed below the tests that use them rather than above.
Rename __timeout_handler to __ignore_alternate_instance_error, since it now swallows access errors too and was easy to confuse with the _snmp_timeout_handler closure elsewhere in the module. Record in the docstring why trapping the base SnmpError is correct and that Snmpv3Error is deliberately caught along with it. Add a test that pushes a failing deferred through _multiquery, covering the errback wiring rather than the handler in isolation. Inline the _make_failure helper, drop the misleading genErr sample message, and reorder helpers below the tests per the step-down rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Thanks for the thorough review, @lunkwill42! Pushed 2dac7b9 addressing the feedback:
While at it, renamed three pre-existing |



Scope and purpose
Fixes #4101.
When ipdevpoll polls a device using community- or context-indexed BRIDGE-MIB
instances, and the device answers a query for one of those alternate instances
with an SNMP error such as
noAccess, the entire job aborts.pynetsnmp surfaces
noAccessas anSnmpErrorreadingPacket for <ip> has error: Permission denied.MultiMibMixIn.__timeout_handleronly ignored
TimeoutErrorfor non-base (alternate) instances, so theSnmpErrorpropagated and aborted the plugin/job. This was observed against Cisco FirePower
(FTD) appliances, which return
noAccessfor community-indexed BRIDGE-MIBqueries, aborting every
topoandinventoryjob for those devices each pollcycle.
This extends
__timeout_handlerto also trapSnmpErrorfor non-base instances,matching the documented intent that errors for anything but the primary instance
should not derail collection. Errors on the base instance still propagate.
How to observe
On an affected device (e.g. Cisco FirePower), a community-indexed query returns
noAccess:Before this change,
ipdevpoll.logshows repeatedJob 'topo'/'inventory' ... aborted: Job aborted due to plugin failure (cause=Packet for <ip> has error: Permission denied). After, those jobs complete and the per-instance error isignored.
This pull request
Contributor Checklist
5.18.x, bugfix for the latest stable version)