Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abort SNMP-based ARP collection if ARP records seem to have been collected earlier in the same ipdevpoll job run #3254

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ to be able to upgrade to Python 3.11:
* :mod:`django-crispy-forms`
* :mod:`crispy-forms-foundation`


NAV 5.12
========
Deprecation warnings
--------------------
.. warning:: The ``[paloaltoarp]`` section of :file:`ipdevpoll.conf`, used for
Expand All @@ -33,6 +30,20 @@ Deprecation warnings
analogous to configuration of SNMP-based fetching. :ref:`See below
for more details<5.12-new-http-rest-api-management-profile-type>`.

Change ``ip2mac`` plugin order in :file:`ipdevpoll.conf`
--------------------------------------------------------

The Palo Alto ARP plugin in ipdevpoll had a problem which caused the ARP
records it collected from Palo Palo firewalls to be unduly closed by the
regular SNMP-based ARP plugin. This release of NAV fixes this by making the
SNMP-based ARP plugin a "fallback" mechanism that doesn't touch ARP collection
if another plugin has already collected ARP data.

In order for this fix to work, **you must change the order of the plugins** in the
lunkwill42 marked this conversation as resolved.
Show resolved Hide resolved
``[job_ip2mac]`` section of your :file:`ipdevpoll.conf` file, to ensure that
the ``paloaltoarp`` plugin is listed *before* the ``arp`` plugin.


.. _5.12-new-http-rest-api-management-profile-type:
New way to configure fetching of Palo Alto firewall ARP cache data
------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/3252.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Stop SNMP-based `arp` plugin from stepping on the `paloaltoarp` plugins toes.
Note that this **requires** updating the plugin order in the `ip2mac` job in `ipdevpoll.conf`
2 changes: 1 addition & 1 deletion python/nav/etc/ipdevpoll.conf
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ description: Checks for changes in the reverse DNS records of devices
interval: 30m
intensity: 0
plugins:
arp paloaltoarp
paloaltoarp arp
description:
The ip2mac job logs IP to MAC address mappings from routers and firewalls
(i.e. from IPv4 ARP and IPv6 Neighbor caches)
Expand Down
21 changes: 16 additions & 5 deletions python/nav/ipdevpoll/plugins/arp.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,10 @@ def can_handle(cls, netbox):

@defer.inlineCallbacks
def handle(self):
# Start by checking the prefix cache
prefix_cache_age = datetime.now() - self.prefix_cache_update_time
if prefix_cache_age > self.prefix_cache_max_age:
yield self._update_prefix_cache()

yield self._check_and_update_prefix_cache()
if self._is_arp_already_collected():
self._logger.debug("ARP records already collected for this device")
return
self._logger.debug("Collecting IP/MAC mappings")

# Fetch standard MIBs
Expand All @@ -99,6 +98,18 @@ def handle(self):

yield self._process_data(mappings)

@classmethod
@defer.inlineCallbacks
def _check_and_update_prefix_cache(cls):
"""Updates the prefix cache if deemed necessary"""
prefix_cache_age = datetime.now() - cls.prefix_cache_update_time
if prefix_cache_age > cls.prefix_cache_max_age:
yield cls._update_prefix_cache()

def _is_arp_already_collected(self):
"""Returns True if ARP entries have already been collected in this run"""
return shadows.Arp in self.containers and bool(self.containers[shadows.Arp])

@defer.inlineCallbacks
def _get_ip_mib(self):
if not self.is_arista():
Expand Down
1 change: 1 addition & 0 deletions python/nav/ipdevpoll/plugins/paloaltoarp.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def can_handle(cls, netbox):
@defer.inlineCallbacks
def handle(self):
"""Handle plugin business, return a deferred."""
self._check_and_update_prefix_cache()
self._logger.debug("Collecting IP/MAC mappings for Paloalto device")

configurations = yield self._get_paloalto_configurations(self.netbox)
Expand Down
37 changes: 37 additions & 0 deletions tests/unittests/ipdevpoll/plugins_arp_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
from datetime import datetime
from unittest.mock import patch

import pytest
import pytest_twisted

from nav.ipdevpoll import shadows
from nav.ipdevpoll.storage import ContainerRepository
from nav.ipdevpoll.plugins.arp import ipv6_address_in_mappings, Arp


class TestCheckAndUpdatePrefixCache:
@pytest.mark.twisted
@pytest_twisted.inlineCallbacks
def test_when_cache_is_expired_it_should_call_update(self):
with patch.object(Arp, "_update_prefix_cache") as update:
Arp.prefix_cache_update_time = datetime.now() - Arp.prefix_cache_max_age * 2
yield Arp._check_and_update_prefix_cache()
assert update.called

@pytest.mark.twisted
@pytest_twisted.inlineCallbacks
def test_when_cache_is_not_expired_it_should_not_call_update(self):
with patch.object(Arp, "_update_prefix_cache") as update:
Arp.prefix_cache_update_time = datetime.now()
yield Arp._check_and_update_prefix_cache()
assert not update.called


def test_none_in_mappings_should_not_raise():
mappings = [(None, None, None)]
assert not ipv6_address_in_mappings(mappings)
Expand All @@ -11,3 +36,15 @@ def test_make_new_mappings_should_not_raise_on_empty_ip():
a = Arp(None, None, ContainerRepository())
mappings = [(None, '00:0b:ad:c0:ff:ee')]
a._make_new_mappings(mappings)


def test_when_arp_records_exist_is_arp_already_collected_should_return_true():
containers = ContainerRepository()
containers.factory(('192.168.0.1', '00:co:ff:ee:ba:be'), shadows.Arp)
plugin = Arp(None, None, containers)
assert plugin._is_arp_already_collected()


def test_when_arp_records_do_not_exist_is_arp_already_collected_should_return_false():
plugin = Arp(None, None, ContainerRepository())
assert not plugin._is_arp_already_collected()
Loading