From 59f92aaa01d7691e99be3595290e05eb698792f0 Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <vzhestkov@suse.com>
Date: Thu, 3 Mar 2022 19:20:43 +0300
Subject: [PATCH] Clear network interface cache when grains are
 requested or refreshed (bsc#1196050)

* test patch

* pre commit

* Add tests for network interfaces cache

* Document test better

* pre commit and changes

Co-authored-by: Joe Eacott <jeacott@vmware.com>
Co-authored-by: Daniel A. Wozniak <dwozniak@saltstack.com>

Co-authored-by: Joe Eacott <31625359+xeacott@users.noreply.github.com>
Co-authored-by: Joe Eacott <jeacott@vmware.com>
Co-authored-by: Daniel A. Wozniak <dwozniak@saltstack.com>
---
 changelog/59490.fixed          |  1 +
 salt/grains/core.py            |  6 ++-
 salt/utils/network.py          | 27 ++++++----
 tests/unit/grains/test_core.py | 90 ++++++++++++++++++++++++++++++++++
 4 files changed, 114 insertions(+), 10 deletions(-)
 create mode 100644 changelog/59490.fixed

diff --git a/changelog/59490.fixed b/changelog/59490.fixed
new file mode 100644
index 0000000000..a523fff36d
--- /dev/null
+++ b/changelog/59490.fixed
@@ -0,0 +1 @@
+Clear the cached network interface grains during minion init and grains refresh
diff --git a/salt/grains/core.py b/salt/grains/core.py
index de838c04b7..c09e14ef47 100644
--- a/salt/grains/core.py
+++ b/salt/grains/core.py
@@ -28,7 +28,7 @@ import warnings
 import time
 import salt.modules.network
 
-from salt.utils.network import _get_interfaces
+from salt.utils.network import _clear_interfaces, _get_interfaces
 
 # pylint: disable=import-error
 try:
@@ -131,6 +131,10 @@ HOST_NOT_FOUND = 1
 NO_DATA = 4
 
 
+def __init__(opts):
+    _clear_interfaces()
+
+
 def _windows_cpudata():
     '''
     Return some CPU information on Windows minions
diff --git a/salt/utils/network.py b/salt/utils/network.py
index 09fb0ac234..35104af770 100644
--- a/salt/utils/network.py
+++ b/salt/utils/network.py
@@ -52,16 +52,25 @@ except (ImportError, OSError, AttributeError, TypeError):
     pass
 
 
-_INTERFACES = {}
-def _get_interfaces(): #! function
-    '''
-    Provide a dict of the connected interfaces and their ip addresses
-    '''
+class Interfaces:
+    __slots__ = ("interfaces",)
+
+    def __init__(self, interfaces=None):
+        if interfaces is None:
+            interfaces = {}
+        self.interfaces = interfaces
+
+    def __call__(self, *args, **kwargs):
+        if not self.interfaces:
+            self.interfaces = interfaces()
+        return self.interfaces
+
+    def clear(self):
+        self.interfaces = {}
+
 
-    global _INTERFACES
-    if not _INTERFACES:
-        _INTERFACES = interfaces()
-    return _INTERFACES
+_get_interfaces = Interfaces()
+_clear_interfaces = _get_interfaces.clear
 
 
 def sanitize_host(host):
diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py
index 5ef882a357..152bccb760 100644
--- a/tests/unit/grains/test_core.py
+++ b/tests/unit/grains/test_core.py
@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
 import logging
 import os
 import socket
+import tempfile
 import textwrap
 import platform
 
@@ -18,6 +19,7 @@ except ImportError as import_error:
     pytest = None
 
 from tests.support.mixins import LoaderModuleMockMixin
+from tests.support.runtests import RUNTIME_VARS
 from tests.support.unit import TestCase, skipIf
 from tests.support.mock import (
     Mock,
@@ -1674,3 +1676,91 @@ class CoreGrainsTestCase(TestCase, LoaderModuleMockMixin):
         exists.return_value = True
         with patch('salt.utils.files.fopen', _fopen):
             self.assertEqual(core._hw_data({'kernel': 'Linux'}), {})
+
+    def test_network_grains_cache(self):
+        """
+        Network interfaces are cache is cleared by the loader
+        """
+        call_1 = {
+            "lo": {
+                "up": True,
+                "hwaddr": "00:00:00:00:00:00",
+                "inet": [
+                    {
+                        "address": "127.0.0.1",
+                        "netmask": "255.0.0.0",
+                        "broadcast": None,
+                        "label": "lo",
+                    }
+                ],
+                "inet6": [],
+            },
+            "wlo1": {
+                "up": True,
+                "hwaddr": "29:9f:9f:e9:67:f4",
+                "inet": [
+                    {
+                        "address": "172.16.13.85",
+                        "netmask": "255.255.248.0",
+                        "broadcast": "172.16.15.255",
+                        "label": "wlo1",
+                    }
+                ],
+                "inet6": [],
+            },
+        }
+        call_2 = {
+            "lo": {
+                "up": True,
+                "hwaddr": "00:00:00:00:00:00",
+                "inet": [
+                    {
+                        "address": "127.0.0.1",
+                        "netmask": "255.0.0.0",
+                        "broadcast": None,
+                        "label": "lo",
+                    }
+                ],
+                "inet6": [],
+            },
+            "wlo1": {
+                "up": True,
+                "hwaddr": "29:9f:9f:e9:67:f4",
+                "inet": [
+                    {
+                        "address": "172.16.13.86",
+                        "netmask": "255.255.248.0",
+                        "broadcast": "172.16.15.255",
+                        "label": "wlo1",
+                    }
+                ],
+                "inet6": [],
+            },
+        }
+        tmp_path = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
+        cache_dir = os.path.join(tmp_path, "cache")
+        extmods = os.path.join(tmp_path, "extmods")
+        opts = {
+            "cachedir": str(cache_dir),
+            "extension_modules": str(extmods),
+            "optimization_order": [0],
+        }
+        with patch(
+            "salt.utils.network.interfaces", side_effect=[call_1, call_2]
+        ) as interfaces:
+            grains = salt.loader.grain_funcs(opts)
+            assert interfaces.call_count == 0
+            ret = grains["core.ip_interfaces"]()
+            # interfaces has been called
+            assert interfaces.call_count == 1
+            assert ret["ip_interfaces"]["wlo1"] == ["172.16.13.85"]
+            # interfaces has been cached
+            ret = grains["core.ip_interfaces"]()
+            assert interfaces.call_count == 1
+            assert ret["ip_interfaces"]["wlo1"] == ["172.16.13.85"]
+
+            grains = salt.loader.grain_funcs(opts)
+            ret = grains["core.ip_interfaces"]()
+            # A new loader clears the cache and interfaces is called again
+            assert interfaces.call_count == 2
+            assert ret["ip_interfaces"]["wlo1"] == ["172.16.13.86"]
-- 
2.35.1


