File include-aliases-in-the-fqdns-grains.patch of Package salt

From 3c956a1cf1de17c5c49f0856051cabe2ffb4d0f2 Mon Sep 17 00:00:00 2001
From: Bo Maryniuk <bo@suse.de>
Date: Tue, 29 Jan 2019 11:11:38 +0100
Subject: [PATCH] Include aliases in the fqdns grains

Add UT for "is_fqdn"

Add "is_fqdn" check to the network utils

Bugfix: include FQDNs aliases

Deprecate UnitTest assertion in favour of built-in assert keyword

Add UT for fqdns aliases

Leverage cached interfaces, if any.
---
 salt/grains/core.py              | 69 +++++++++++++++++++++-----------
 salt/utils/network.py            | 16 ++++++++
 tests/unit/grains/test_core.py   | 45 ++++++++++++++++++---
 tests/unit/utils/test_network.py | 37 +++++++++++++++++
 4 files changed, 138 insertions(+), 29 deletions(-)

diff --git a/salt/grains/core.py b/salt/grains/core.py
index bc3cf129cd..006878f806 100644
--- a/salt/grains/core.py
+++ b/salt/grains/core.py
@@ -1733,29 +1733,31 @@ def _parse_cpe_name(cpe):
 
 
 def _parse_cpe_name(cpe):
-    '''
+    """
     Parse CPE_NAME data from the os-release
 
     Info: https://csrc.nist.gov/projects/security-content-automation-protocol/scap-specifications/cpe
 
     :param cpe:
     :return:
-    '''
+    """
     part = {
-        'o': 'operating system',
-        'h': 'hardware',
-        'a': 'application',
+        "o": "operating system",
+        "h": "hardware",
+        "a": "application",
     }
     ret = {}
-    cpe = (cpe or '').split(':')
-    if len(cpe) > 4 and cpe[0] == 'cpe':
-        if cpe[1].startswith('/'):  # WFN to URI
-            ret['vendor'], ret['product'], ret['version'] = cpe[2:5]
-            ret['phase'] = cpe[5] if len(cpe) > 5 else None
-            ret['part'] = part.get(cpe[1][1:])
-        elif len(cpe) == 13 and cpe[1] == '2.3':  # WFN to a string
-            ret['vendor'], ret['product'], ret['version'], ret['phase'] = [x if x != '*' else None for x in cpe[3:7]]
-            ret['part'] = part.get(cpe[2])
+    cpe = (cpe or "").split(":")
+    if len(cpe) > 4 and cpe[0] == "cpe":
+        if cpe[1].startswith("/"):  # WFN to URI
+            ret["vendor"], ret["product"], ret["version"] = cpe[2:5]
+            ret["phase"] = cpe[5] if len(cpe) > 5 else None
+            ret["part"] = part.get(cpe[1][1:])
+        elif len(cpe) == 13 and cpe[1] == "2.3":  # WFN to a string
+            ret["vendor"], ret["product"], ret["version"], ret["phase"] = [
+                x if x != "*" else None for x in cpe[3:7]
+            ]
+            ret["part"] = part.get(cpe[2])
 
     return ret
 
@@ -2396,15 +2398,36 @@ def fqdns():
     """
     # Provides:
     # fqdns
-    opt = {"fqdns": []}
-    if __opts__.get(
-        "enable_fqdns_grains",
-        False
-        if salt.utils.platform.is_windows() or salt.utils.platform.is_proxy()
-        else True,
-    ):
-        opt = __salt__["network.fqdns"]()
-    return opt
+
+    grains = {}
+    fqdns = set()
+
+    addresses = salt.utils.network.ip_addrs(
+        include_loopback=False, interface_data=_get_interfaces()
+    )
+    addresses.extend(
+        salt.utils.network.ip_addrs6(
+            include_loopback=False, interface_data=_get_interfaces()
+        )
+    )
+    err_message = "Exception during resolving address: %s"
+    for ip in addresses:
+        try:
+            name, aliaslist, addresslist = socket.gethostbyaddr(ip)
+            fqdns.update(
+                [socket.getfqdn(name)]
+                + [als for als in aliaslist if salt.utils.network.is_fqdn(als)]
+            )
+        except socket.herror as err:
+            if err.errno in (0, HOST_NOT_FOUND, NO_DATA):
+                # No FQDN for this IP address, so we don't need to know this all the time.
+                log.debug("Unable to resolve address %s: %s", ip, err)
+            else:
+                log.error(err_message, ip, err)
+        except (OSError, socket.gaierror, socket.timeout) as err:
+            log.error(err_message, ip, err)
+
+    return {"fqdns": sorted(list(fqdns))}
 
 
 def ip_fqdn():
diff --git a/salt/utils/network.py b/salt/utils/network.py
index b3e8db3886..dd7fceb91a 100644
--- a/salt/utils/network.py
+++ b/salt/utils/network.py
@@ -2208,3 +2208,19 @@ def filter_by_networks(values, networks):
             raise ValueError("Do not know how to filter a {}".format(type(values)))
     else:
         return values
+
+
+def is_fqdn(hostname):
+    """
+    Verify if hostname conforms to be a FQDN.
+
+    :param hostname: text string with the name of the host
+    :return: bool, True if hostname is correct FQDN, False otherwise
+    """
+
+    compliant = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(?<!-)$", re.IGNORECASE)
+    return (
+        "." in hostname
+        and len(hostname) < 0xFF
+        and all(compliant.match(x) for x in hostname.rstrip(".").split("."))
+    )
diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py
index 7dbf34deac..d760e57a54 100644
--- a/tests/unit/grains/test_core.py
+++ b/tests/unit/grains/test_core.py
@@ -1367,12 +1367,11 @@ class CoreGrainsTestCase(TestCase, LoaderModuleMockMixin):
             ("bluesniff.foo.bar", [], ["fe80::a8b2:93ff:dead:beef"]),
         ]
         ret = {"fqdns": ["bluesniff.foo.bar", "foo.bar.baz", "rinzler.evil-corp.com"]}
-        with patch.dict(core.__salt__, {"network.fqdns": salt.modules.network.fqdns}):
-            with patch.object(socket, "gethostbyaddr", side_effect=reverse_resolv_mock):
-                fqdns = core.fqdns()
-                assert "fqdns" in fqdns
-                assert len(fqdns["fqdns"]) == len(ret["fqdns"])
-                assert set(fqdns["fqdns"]) == set(ret["fqdns"])
+        with patch.object(socket, "gethostbyaddr", side_effect=reverse_resolv_mock):
+            fqdns = core.fqdns()
+            assert "fqdns" in fqdns
+            assert len(fqdns["fqdns"]) == len(ret["fqdns"])
+            assert set(fqdns["fqdns"]) == set(ret["fqdns"])
 
     @skipIf(not salt.utils.platform.is_linux(), "System is not Linux")
     @patch("salt.utils.network.ip_addrs", MagicMock(return_value=["1.2.3.4"]))
@@ -1413,6 +1412,40 @@ class CoreGrainsTestCase(TestCase, LoaderModuleMockMixin):
                     mock_log.debug.assert_called_once()
                     mock_log.error.assert_called()
 
+    @patch.object(salt.utils.platform, "is_windows", MagicMock(return_value=False))
+    @patch(
+        "salt.utils.network.ip_addrs", MagicMock(return_value=["1.2.3.4", "5.6.7.8"])
+    )
+    @patch(
+        "salt.utils.network.ip_addrs6",
+        MagicMock(return_value=["fe80::a8b2:93ff:fe00:0", "fe80::a8b2:93ff:dead:beef"]),
+    )
+    @patch(
+        "salt.utils.network.socket.getfqdn", MagicMock(side_effect=lambda v: v)
+    )  # Just pass-through
+    def test_fqdns_aliases(self):
+        """
+        FQDNs aliases
+        """
+        reverse_resolv_mock = [
+            ("foo.bar.baz", ["throwmeaway", "this.is.valid.alias"], ["1.2.3.4"]),
+            ("rinzler.evil-corp.com", ["false-hostname", "badaliass"], ["5.6.7.8"]),
+            ("foo.bar.baz", [], ["fe80::a8b2:93ff:fe00:0"]),
+            (
+                "bluesniff.foo.bar",
+                ["alias.bluesniff.foo.bar"],
+                ["fe80::a8b2:93ff:dead:beef"],
+            ),
+        ]
+        with patch.object(socket, "gethostbyaddr", side_effect=reverse_resolv_mock):
+            fqdns = core.fqdns()
+            assert "fqdns" in fqdns
+            for alias in ["this.is.valid.alias", "alias.bluesniff.foo.bar"]:
+                assert alias in fqdns["fqdns"]
+
+            for alias in ["throwmeaway", "false-hostname", "badaliass"]:
+                assert alias not in fqdns["fqdns"]
+
     def test_core_virtual(self):
         """
         test virtual grain with cmd virt-what
diff --git a/tests/unit/utils/test_network.py b/tests/unit/utils/test_network.py
index 779fc0fc34..9a37a94d8f 100644
--- a/tests/unit/utils/test_network.py
+++ b/tests/unit/utils/test_network.py
@@ -1274,3 +1274,40 @@ class NetworkTestCase(TestCase):
             ),
         ):
             self.assertEqual(network.get_fqhostname(), host)
+
+    def test_netlink_tool_remote_on(self):
+        with patch("subprocess.check_output", return_value=NETLINK_SS):
+            remotes = network._netlink_tool_remote_on("4505", "remote")
+            self.assertEqual(remotes, {"127.0.0.1", "::ffff:1.2.3.4"})
+
+    def test_is_fqdn(self):
+        """
+        Test is_fqdn function passes possible FQDN names.
+
+        :return: None
+        """
+        for fqdn in [
+            "host.domain.com",
+            "something.with.the.dots.still.ok",
+            "UPPERCASE.ALSO.SHOULD.WORK",
+            "MiXeD.CaSe.AcCePtAbLe",
+            "123.host.com",
+            "host123.com",
+            "some_underscore.com",
+            "host-here.com",
+        ]:
+            assert network.is_fqdn(fqdn)
+
+    def test_is_not_fqdn(self):
+        """
+        Test is_fqdn function rejects FQDN names.
+
+        :return: None
+        """
+        for fqdn in [
+            "hostname",
+            "/some/path",
+            "$variable.here",
+            "verylonghostname.{}".format("domain" * 45),
+        ]:
+            assert not network.is_fqdn(fqdn)
-- 
2.29.2
openSUSE Build Service is sponsored by