From e8fc3e770aba4c4bba46e559146ab198cd271f54 Mon Sep 17 00:00:00 2001
From: Bo Maryniuk <bo@suse.de>
Date: Thu, 2 Jun 2016 17:24:17 +0200
Subject: [PATCH 09/38] Rewrite minion ID generator (bsc#967803)

* Remove hostname sorting
* Keep order on deduplication
* Fix logical expression
* Rewrite generate_minion_id function
* Use api of the distinct list
* Use FQDN at last, as it may differ
* Rename network unit test into a proper name
* Fix network test for Solaris
* Get hostname as an attribute of a raw socket.
* Make generate_minioin_id more testable.
* Add unit test for check if the minion ID is distinct in the pool
* Add unit test to check on duplicate hostnames in the pool
* Add test for the first in line from platform.node is used (mostly)
* Add unit test to filter out localhost from the first occurrence
* Add unit test to check if any of localhost or local IP addresses are filtered
* Add unit test check for non-localhost IP is accepted as a minion ID
* Fix the documentation
* Fix lint
* Remove unnecessary tests: no fallback to what was already looked at
* Return 'localhost' if no other name has been found.
* Check raw socket attribute only if other socket checks had failed.
* Test fix: look for attribute name only when other socket checks had failed
* Test fix: only non-localhost IP should persist
* Add unit test: minion ID should be localhost, if everything is localhost.
* Add unit test: check if FQDN is picked up
* Add unit test: addrinfo from the raw socket should be picked up if all other socket checks had failed to localhost
* Do not use cached localhost name.
* Fix lint: unused import

* Regression fix: minion ID generator should use FQDN first, if available (#34876)
* Regression fix: use FQDN first, if available
* Adjust the tests to the new behaviour (FQDN first)
---
 salt/config.py                   |   2 +-
 salt/utils/network.py            | 260 ++++++++----------------------
 tests/unit/config_test.py        |  61 ++++----
 tests/unit/utils/network.py      | 193 -----------------------
 tests/unit/utils/network_test.py | 331 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 424 insertions(+), 423 deletions(-)
 delete mode 100644 tests/unit/utils/network.py
 create mode 100644 tests/unit/utils/network_test.py

diff --git a/salt/config.py b/salt/config.py
index 968ceeb..9ad1d6a 100644
--- a/salt/config.py
+++ b/salt/config.py
@@ -2816,7 +2816,7 @@ def get_id(opts, cache_minion_id=False):
                 bname = salt.utils.to_bytes(name)
                 if bname.startswith(codecs.BOM):  # Remove BOM if exists
                     name = salt.utils.to_str(bname.replace(codecs.BOM, '', 1))
-            if name:
+            if name and name != 'localhost':
                 log.debug('Using cached minion ID from {0}: {1}'.format(id_cache, name))
                 return name, False
         except (IOError, OSError):
diff --git a/salt/utils/network.py b/salt/utils/network.py
index fb89e9c..dfda364 100644
--- a/salt/utils/network.py
+++ b/salt/utils/network.py
@@ -10,6 +10,7 @@ import re
 import shlex
 import socket
 import logging
+import platform
 from string import ascii_letters, digits
 
 # Import 3rd-party libs
@@ -72,207 +73,78 @@ def host_to_ip(host):
     return ip
 
 
-def _filter_localhost_names(name_list):
-    '''
-    Returns list without local hostnames and ip addresses.
-    '''
-    h = []
-    re_filters = [
-        'localhost.*',
-        'ip6-.*',
-        '127.*',
-        r'0\.0\.0\.0',
-        '::1.*',
-        'fe00::.*',
-        'fe02::.*',
-        '1.0.0.*.ip6.arpa',
-    ]
-    for name in name_list:
-        filtered = False
-        for f in re_filters:
-            if re.match(f, name):
-                filtered = True
-                break
-        if not filtered:
-            h.append(name)
-    return h
-
-
-def _sort_hostnames(hostname_list):
-    '''
-    sort minion ids favoring in order of:
-        - FQDN
-        - public ipaddress
-        - localhost alias
-        - private ipaddress
-    '''
-    # punish matches in order of preference
-    punish = [
-        'localhost.localdomain',
-        'localhost.my.domain',
-        'localhost4.localdomain4',
-        'localhost',
-        'ip6-localhost',
-        'ip6-loopback',
-        'ipv6-localhost',
-        'ipv6-loopback',
-        '127.0.2.1',
-        '127.0.1.1',
-        '127.0.0.1',
-        '0.0.0.0',
-        '::1',
-        'fe00::',
-        'fe02::',
-    ]
-
-    def _key_hostname(e):
-        # should never have a space in hostname
-        # favor hostnames w/o spaces
-        if ' ' in e:
-            first = 1
-        else:
-            first = -1
-
-        # punish localhost list
-        if e in punish:
-            second = punish.index(e)
-        else:
-            second = -1
-
-        # punish ipv6
-        third = e.count(':')
-
-        # punish ipv4
-        # punish ipv4 addresses that start with '127.' more
-        e_is_ipv4 = e.count('.') == 3 and not any(c.isalpha() for c in e)
-        if e_is_ipv4:
-            if e.startswith('127.'):
-                fourth = 1
-            else:
-                fourth = 0
-        else:
-            fourth = -1
-
-        # favor hosts with more dots
-        fifth = -(e.count('.'))
-
-        # favor longest fqdn
-        sixth = -(len(e))
-
-        return (first, second, third, fourth, fifth, sixth)
-
-    return sorted(hostname_list, key=_key_hostname)
-
-
-def get_hostnames():
-    '''
-    Get list of hostnames using multiple strategies
+def _generate_minion_id():
     '''
-    h = []
-    h.append(socket.gethostname())
-    h.append(socket.getfqdn())
+    Get list of possible host names and convention names.
 
-    # try socket.getaddrinfo
-    try:
-        addrinfo = socket.getaddrinfo(
-            socket.gethostname(), 0, socket.AF_UNSPEC, socket.SOCK_STREAM,
-            socket.SOL_TCP, socket.AI_CANONNAME
-        )
-        for info in addrinfo:
-            # info struct [family, socktype, proto, canonname, sockaddr]
-            if len(info) >= 4:
-                h.append(info[3])
-    except socket.gaierror:
-        pass
-
-    # try /etc/hostname
-    try:
-        name = ''
-        with salt.utils.fopen('/etc/hostname') as hfl:
-            name = hfl.read()
-        h.append(name)
-    except (IOError, OSError):
-        pass
+    :return:
+    '''
+    # There are three types of hostnames:
+    # 1. Network names. How host is accessed from the network.
+    # 2. Host aliases. They might be not available in all the network or only locally (/etc/hosts)
+    # 3. Convention names, an internal nodename.
 
-    # try /etc/nodename (SunOS only)
-    if salt.utils.is_sunos():
+    class DistinctList(list):
+        '''
+        List, which allows to append only distinct objects.
+        Needs to work on Python 2.6, because of collections.OrderedDict only since 2.7 version.
+        Override 'filter()' for custom filtering.
+        '''
+        localhost_matchers = ['localhost.*', 'ip6-.*', '127.*', r'0\.0\.0\.0',
+                              '::1.*', 'ipv6-.*', 'fe00::.*', 'fe02::.*', '1.0.0.*.ip6.arpa']
+
+        def append(self, p_object):
+            if p_object and p_object not in self and not self.filter(p_object):
+                super(self.__class__, self).append(p_object)
+            return self
+
+        def extend(self, iterable):
+            for obj in iterable:
+                self.append(obj)
+            return self
+
+        def filter(self, element):
+            'Returns True if element needs to be filtered'
+            for rgx in self.localhost_matchers:
+                if re.match(rgx, element):
+                    return True
+
+        def first(self):
+            return self and self[0] or None
+
+    hosts = DistinctList().append(socket.getfqdn()).append(platform.node()).append(socket.gethostname())
+    if not hosts:
         try:
-            name = ''
-            with salt.utils.fopen('/etc/nodename') as hfl:
-                name = hfl.read()
-            h.append(name)
-        except (IOError, OSError):
-            pass
-
-    # try /etc/hosts
-    try:
-        with salt.utils.fopen('/etc/hosts') as hfl:
-            for line in hfl:
-                names = line.split()
-                try:
-                    ip = names.pop(0)
-                except IndexError:
-                    continue
-                if ip.startswith('127.') or ip == '::1':
-                    for name in names:
-                        h.append(name)
-    except (IOError, OSError):
-        pass
+            for a_nfo in socket.getaddrinfo(hosts.first(), None, socket.AF_INET,
+                                            socket.SOCK_RAW, socket.IPPROTO_IP, socket.AI_CANONNAME):
+                if len(a_nfo) > 3:
+                    hosts.append(a_nfo[3])
+        except socket.gaierror:
+            log.warn('Cannot resolve address {addr} info via socket: {message}'.format(
+                addr=hosts.first(), message=socket.gaierror)
+            )
+    # Universal method for everywhere (Linux, Slowlaris, Windows etc)
+    for f_name in ['/etc/hostname', '/etc/nodename', '/etc/hosts',
+                   r'{win}\system32\drivers\etc\hosts'.format(win=os.getenv('WINDIR'))]:
+        if not os.path.exists(f_name):
+            continue
+        with salt.utils.fopen(f_name) as f_hdl:
+            for hst in (line.strip().split('#')[0].strip().split() or None for line in f_hdl.read().split(os.linesep)):
+                if hst and (hst[0][:4] in ['127.', '::1'] or len(hst) == 1):
+                    hosts.extend(hst)
 
-    # try windows hosts
-    if salt.utils.is_windows():
-        try:
-            windir = os.getenv('WINDIR')
-            with salt.utils.fopen(windir + r'\system32\drivers\etc\hosts') as hfl:
-                for line in hfl:
-                    # skip commented or blank lines
-                    if line[0] == '#' or len(line) <= 1:
-                        continue
-                    # process lines looking for '127.' in first column
-                    try:
-                        entry = line.split()
-                        if entry[0].startswith('127.'):
-                            for name in entry[1:]:  # try each name in the row
-                                h.append(name)
-                    except IndexError:
-                        pass  # could not split line (malformed entry?)
-        except (IOError, OSError):
-            pass
-
-    # strip spaces and ignore empty strings
-    hosts = []
-    for name in h:
-        name = name.strip()
-        if len(name) > 0:
-            hosts.append(name)
-
-    # remove duplicates
-    hosts = list(set(hosts))
-    return hosts
+    # include public and private ipaddresses
+    return hosts.extend([addr for addr in salt.utils.network.ip_addrs()
+                         if not ipaddress.ip_address(addr).is_loopback])
 
 
 def generate_minion_id():
     '''
-    Returns a minion id after checking multiple sources for a FQDN.
-    If no FQDN is found you may get an ip address
-    '''
-    possible_ids = get_hostnames()
+    Return only first element of the hostname from all possible list.
 
-    # include public and private ipaddresses
-    for addr in salt.utils.network.ip_addrs():
-        addr = ipaddress.ip_address(addr)
-        if addr.is_loopback:
-            continue
-        possible_ids.append(str(addr))
-
-    possible_ids = _filter_localhost_names(possible_ids)
-
-    # if no minion id
-    if len(possible_ids) == 0:
-        return 'noname'
-
-    hosts = _sort_hostnames(possible_ids)
-    return hosts[0]
+    :return:
+    '''
+    return _generate_minion_id().first() or 'localhost'
 
 
 def get_socket(addr, type=socket.SOCK_STREAM, proto=0):
@@ -309,11 +181,7 @@ def get_fqhostname():
     except socket.gaierror:
         pass
 
-    l = _sort_hostnames(l)
-    if len(l) > 0:
-        return l[0]
-
-    return None
+    return l and l[0] or None
 
 
 def ip_to_host(ip):
diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py
index 9ba3819..ee5d202 100644
--- a/tests/unit/config_test.py
+++ b/tests/unit/config_test.py
@@ -13,7 +13,6 @@ import logging
 import os
 import shutil
 import tempfile
-from contextlib import contextmanager
 
 # Import Salt Testing libs
 from salttesting import TestCase
@@ -66,38 +65,6 @@ def _unhandled_mock_read(filename):
     raise CommandExecutionError('Unhandled mock read for {0}'.format(filename))
 
 
-@contextmanager
-def _fopen_side_effect_etc_hostname(filename):
-    '''
-    Mock reading from /etc/hostname
-    '''
-    log.debug('Mock-reading {0}'.format(filename))
-    if filename == '/etc/hostname':
-        mock_open = MagicMock()
-        mock_open.read.return_value = MOCK_ETC_HOSTNAME
-        yield mock_open
-    elif filename == '/etc/hosts':
-        raise IOError(2, "No such file or directory: '{0}'".format(filename))
-    else:
-        _unhandled_mock_read(filename)
-
-
-@contextmanager
-def _fopen_side_effect_etc_hosts(filename):
-    '''
-    Mock /etc/hostname not existing, and falling back to reading /etc/hosts
-    '''
-    log.debug('Mock-reading {0}'.format(filename))
-    if filename == '/etc/hostname':
-        raise IOError(2, "No such file or directory: '{0}'".format(filename))
-    elif filename == '/etc/hosts':
-        mock_open = MagicMock()
-        mock_open.__iter__.return_value = MOCK_ETC_HOSTS.splitlines()
-        yield mock_open
-    else:
-        _unhandled_mock_read(filename)
-
-
 class ConfigTestCase(TestCase, integration.AdaptedConfigurationTestCaseMixIn):
     def test_proper_path_joining(self):
         fpath = tempfile.mktemp()
@@ -373,6 +340,34 @@ class ConfigTestCase(TestCase, integration.AdaptedConfigurationTestCaseMixIn):
         self.assertEqual(syndic_opts['_master_conf_file'], minion_conf_path)
         self.assertEqual(syndic_opts['_minion_conf_file'], syndic_conf_path)
 
+    def test_issue_6714_parsing_errors_logged(self):
+        try:
+            tempdir = tempfile.mkdtemp(dir=integration.SYS_TMP_DIR)
+            test_config = os.path.join(tempdir, 'config')
+
+            # Let's populate a master configuration file with some basic
+            # settings
+            salt.utils.fopen(test_config, 'w').write(
+                'root_dir: {0}\n'
+                'log_file: {0}/foo.log\n'.format(tempdir) +
+                '\n\n\n'
+                'blah:false\n'
+            )
+
+            with TestsLoggingHandler() as handler:
+                # Let's load the configuration
+                config = sconfig.master_config(test_config)
+                for message in handler.messages:
+                    if message.startswith('ERROR:Error parsing configuration'):
+                        break
+                else:
+                    raise AssertionError(
+                        'No parsing error message was logged'
+                    )
+        finally:
+            if os.path.isdir(tempdir):
+                shutil.rmtree(tempdir)
+
     @patch('salt.utils.network.get_fqhostname', MagicMock(return_value='localhost'))
     def test_get_id_etc_hostname(self):
         '''
diff --git a/tests/unit/utils/network.py b/tests/unit/utils/network.py
deleted file mode 100644
index 89db848..0000000
--- a/tests/unit/utils/network.py
+++ /dev/null
@@ -1,193 +0,0 @@
-# -*- coding: utf-8 -*-
-# Import Python libs
-from __future__ import absolute_import
-
-# Import Salt Testing libs
-from salttesting import skipIf
-from salttesting import TestCase
-from salttesting.helpers import ensure_in_syspath
-from salttesting.mock import NO_MOCK, NO_MOCK_REASON, patch
-ensure_in_syspath('../../')
-
-# Import salt libs
-from salt.utils import network
-
-LINUX = '''\
-eth0      Link encap:Ethernet  HWaddr e0:3f:49:85:6a:af
-          inet addr:10.10.10.56  Bcast:10.10.10.255  Mask:255.255.252.0
-          inet6 addr: fe80::e23f:49ff:fe85:6aaf/64 Scope:Link
-          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
-          RX packets:643363 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:196539 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:1000
-          RX bytes:386388355 (368.4 MiB)  TX bytes:25600939 (24.4 MiB)
-
-lo        Link encap:Local Loopback
-          inet addr:127.0.0.1  Mask:255.0.0.0
-          inet6 addr: ::1/128 Scope:Host
-          UP LOOPBACK RUNNING  MTU:65536  Metric:1
-          RX packets:548901 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:548901 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:0
-          RX bytes:613479895 (585.0 MiB)  TX bytes:613479895 (585.0 MiB)
-'''
-
-FREEBSD = '''
-em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
-        options=4219b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,WOL_MAGIC,VLAN_HWTSO>
-        ether 00:30:48:ff:ff:ff
-        inet 10.10.10.250 netmask 0xffffffe0 broadcast 10.10.10.255
-        inet 10.10.10.56 netmask 0xffffffc0 broadcast 10.10.10.63
-        media: Ethernet autoselect (1000baseT <full-duplex>)
-        status: active
-em1: flags=8c02<BROADCAST,OACTIVE,SIMPLEX,MULTICAST> metric 0 mtu 1500
-        options=4219b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,WOL_MAGIC,VLAN_HWTSO>
-        ether 00:30:48:aa:aa:aa
-        media: Ethernet autoselect
-        status: no carrier
-plip0: flags=8810<POINTOPOINT,SIMPLEX,MULTICAST> metric 0 mtu 1500
-lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
-        options=3<RXCSUM,TXCSUM>
-        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x8
-        inet6 ::1 prefixlen 128
-        inet 127.0.0.1 netmask 0xff000000
-        nd6 options=3<PERFORMNUD,ACCEPT_RTADV>
-tun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> metric 0 mtu 1500
-        options=80000<LINKSTATE>
-        inet 10.12.0.1 --> 10.12.0.2 netmask 0xffffffff
-        Opened by PID 1964
-'''
-
-SOLARIS = '''\
-lo0: flags=2001000849<UP,LOOPBACK,RUNNING,MULTICAST,IPv4,VIRTUAL> mtu 8232 index 1
-        inet 127.0.0.1 netmask ff000000
-net0: flags=100001100943<UP,BROADCAST,RUNNING,PROMISC,MULTICAST,ROUTER,IPv4,PHYSRUNNING> mtu 1500 index 2
-        inet 10.10.10.38 netmask ffffffe0 broadcast 10.10.10.63
-ilbint0: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 3
-        inet 10.6.0.11 netmask ffffff00 broadcast 10.6.0.255
-ilbext0: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 4
-        inet 10.10.11.11 netmask ffffffe0 broadcast 10.10.11.31
-ilbext0:1: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 4
-        inet 10.10.11.12 netmask ffffffe0 broadcast 10.10.11.31
-vpn0: flags=1000011008d1<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST,ROUTER,IPv4,PHYSRUNNING> mtu 1480 index 5
-        inet tunnel src 10.10.11.12 tunnel dst 10.10.5.5
-        tunnel hop limit 64
-        inet 10.6.0.14 --> 10.6.0.15 netmask ff000000
-lo0: flags=2002000849<UP,LOOPBACK,RUNNING,MULTICAST,IPv6,VIRTUAL> mtu 8252 index 1
-        inet6 ::1/128
-net0: flags=120002004941<UP,RUNNING,PROMISC,MULTICAST,DHCP,IPv6,PHYSRUNNING> mtu 1500 index 2
-        inet6 fe80::221:9bff:fefd:2a22/10
-ilbint0: flags=120002000840<RUNNING,MULTICAST,IPv6,PHYSRUNNING> mtu 1500 index 3
-        inet6 ::/0
-ilbext0: flags=120002000840<RUNNING,MULTICAST,IPv6,PHYSRUNNING> mtu 1500 index 4
-        inet6 ::/0
-vpn0: flags=120002200850<POINTOPOINT,RUNNING,MULTICAST,NONUD,IPv6,PHYSRUNNING> mtu 1480 index 5
-        inet tunnel src 10.10.11.12 tunnel dst 10.10.5.5
-        tunnel hop limit 64
-        inet6 ::/0 --> fe80::b2d6:7c10
-'''
-
-FREEBSD_SOCKSTAT = '''\
-USER    COMMAND     PID     FD  PROTO  LOCAL ADDRESS    FOREIGN ADDRESS
-root    python2.7   1294    41  tcp4   127.0.0.1:61115  127.0.0.1:4506
-'''
-
-
-@skipIf(NO_MOCK, NO_MOCK_REASON)
-class NetworkTestCase(TestCase):
-
-    def test_interfaces_ifconfig_linux(self):
-        interfaces = network._interfaces_ifconfig(LINUX)
-        self.assertEqual(interfaces,
-                         {'eth0': {'hwaddr': 'e0:3f:49:85:6a:af',
-                                   'inet': [{'address': '10.10.10.56',
-                                             'broadcast': '10.10.10.255',
-                                             'netmask': '255.255.252.0'}],
-                                   'inet6': [{'address': 'fe80::e23f:49ff:fe85:6aaf',
-                                              'prefixlen': '64',
-                                              'scope': 'link'}],
-                                   'up': True},
-                          'lo': {'inet': [{'address': '127.0.0.1',
-                                           'netmask': '255.0.0.0'}],
-                                 'inet6': [{'address': '::1',
-                                            'prefixlen': '128',
-                                            'scope': 'host'}],
-                                 'up': True}}
-        )
-
-    def test_interfaces_ifconfig_freebsd(self):
-        interfaces = network._interfaces_ifconfig(FREEBSD)
-        self.assertEqual(interfaces,
-                         {'': {'up': False},
-                          'em0': {'hwaddr': '00:30:48:ff:ff:ff',
-                                  'inet': [{'address': '10.10.10.250',
-                                            'broadcast': '10.10.10.255',
-                                            'netmask': '255.255.255.224'},
-                                           {'address': '10.10.10.56',
-                                            'broadcast': '10.10.10.63',
-                                            'netmask': '255.255.255.192'}],
-                                  'up': True},
-                          'em1': {'hwaddr': '00:30:48:aa:aa:aa',
-                                  'up': False},
-                          'lo0': {'inet': [{'address': '127.0.0.1',
-                                            'netmask': '255.0.0.0'}],
-                                  'inet6': [{'address': 'fe80::1',
-                                             'prefixlen': '64',
-                                             'scope': '0x8'},
-                                            {'address': '::1',
-                                             'prefixlen': '128',
-                                             'scope': None}],
-                                  'up': True},
-                          'plip0': {'up': False},
-                          'tun0': {'inet': [{'address': '10.12.0.1',
-                                             'netmask': '255.255.255.255'}],
-                                   'up': True}}
-
-        )
-
-    def test_interfaces_ifconfig_solaris(self):
-        with patch('salt.utils.is_sunos', lambda: True):
-            interfaces = network._interfaces_ifconfig(SOLARIS)
-            self.assertEqual(interfaces,
-                             {'ilbext0': {'inet': [{'address': '10.10.11.11',
-                                                    'broadcast': '10.10.11.31',
-                                                    'netmask': '255.255.255.224'}],
-                                          'inet6': [{'address': '::',
-                                                     'prefixlen': '0'}],
-                                          'up': True},
-                              'ilbint0': {'inet': [{'address': '10.6.0.11',
-                                                    'broadcast': '10.6.0.255',
-                                                    'netmask': '255.255.255.0'}],
-                                          'inet6': [{'address': '::',
-                                                     'prefixlen': '0'}],
-                                          'up': True},
-                              'lo0': {'inet': [{'address': '127.0.0.1',
-                                                'netmask': '255.0.0.0'}],
-                                      'inet6': [{'address': '::1',
-                                                 'prefixlen': '128'}],
-                                      'up': True},
-                              'net0': {'inet': [{'address': '10.10.10.38',
-                                                 'broadcast': '10.10.10.63',
-                                                 'netmask': '255.255.255.224'}],
-                                       'inet6': [{'address': 'fe80::221:9bff:fefd:2a22',
-                                                  'prefixlen': '10'}],
-                                       'up': True},
-                              'vpn0': {'inet': [{'address': '10.6.0.14',
-                                                 'netmask': '255.0.0.0'}],
-                                       'inet6': [{'address': '::',
-                                                  'prefixlen': '0'}],
-                                       'up': True}}
-            )
-
-    def test_freebsd_remotes_on(self):
-        with patch('salt.utils.is_sunos', lambda: False):
-            with patch('salt.utils.is_freebsd', lambda: True):
-                with patch('subprocess.check_output',
-                           return_value=FREEBSD_SOCKSTAT):
-                    remotes = network._freebsd_remotes_on('4506', 'remote')
-                    self.assertEqual(remotes, set(['127.0.0.1']))
-
-
-if __name__ == '__main__':
-    from integration import run_tests
-    run_tests(NetworkTestCase, needs_daemon=False)
diff --git a/tests/unit/utils/network_test.py b/tests/unit/utils/network_test.py
new file mode 100644
index 0000000..f336588
--- /dev/null
+++ b/tests/unit/utils/network_test.py
@@ -0,0 +1,331 @@
+# -*- coding: utf-8 -*-
+# Import Python libs
+from __future__ import absolute_import
+
+# Import Salt Testing libs
+from salttesting import skipIf
+from salttesting import TestCase
+from salttesting.helpers import ensure_in_syspath
+from salttesting.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock
+ensure_in_syspath('../../')
+
+# Import salt libs
+from salt.utils import network
+
+LINUX = '''\
+eth0      Link encap:Ethernet  HWaddr e0:3f:49:85:6a:af
+          inet addr:10.10.10.56  Bcast:10.10.10.255  Mask:255.255.252.0
+          inet6 addr: fe80::e23f:49ff:fe85:6aaf/64 Scope:Link
+          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
+          RX packets:643363 errors:0 dropped:0 overruns:0 frame:0
+          TX packets:196539 errors:0 dropped:0 overruns:0 carrier:0
+          collisions:0 txqueuelen:1000
+          RX bytes:386388355 (368.4 MiB)  TX bytes:25600939 (24.4 MiB)
+
+lo        Link encap:Local Loopback
+          inet addr:127.0.0.1  Mask:255.0.0.0
+          inet6 addr: ::1/128 Scope:Host
+          UP LOOPBACK RUNNING  MTU:65536  Metric:1
+          RX packets:548901 errors:0 dropped:0 overruns:0 frame:0
+          TX packets:548901 errors:0 dropped:0 overruns:0 carrier:0
+          collisions:0 txqueuelen:0
+          RX bytes:613479895 (585.0 MiB)  TX bytes:613479895 (585.0 MiB)
+'''
+
+FREEBSD = '''
+em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
+        options=4219b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,WOL_MAGIC,VLAN_HWTSO>
+        ether 00:30:48:ff:ff:ff
+        inet 10.10.10.250 netmask 0xffffffe0 broadcast 10.10.10.255
+        inet 10.10.10.56 netmask 0xffffffc0 broadcast 10.10.10.63
+        media: Ethernet autoselect (1000baseT <full-duplex>)
+        status: active
+em1: flags=8c02<BROADCAST,OACTIVE,SIMPLEX,MULTICAST> metric 0 mtu 1500
+        options=4219b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,WOL_MAGIC,VLAN_HWTSO>
+        ether 00:30:48:aa:aa:aa
+        media: Ethernet autoselect
+        status: no carrier
+plip0: flags=8810<POINTOPOINT,SIMPLEX,MULTICAST> metric 0 mtu 1500
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
+        options=3<RXCSUM,TXCSUM>
+        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x8
+        inet6 ::1 prefixlen 128
+        inet 127.0.0.1 netmask 0xff000000
+        nd6 options=3<PERFORMNUD,ACCEPT_RTADV>
+tun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> metric 0 mtu 1500
+        options=80000<LINKSTATE>
+        inet 10.12.0.1 --> 10.12.0.2 netmask 0xffffffff
+        Opened by PID 1964
+'''
+
+SOLARIS = '''\
+lo0: flags=2001000849<UP,LOOPBACK,RUNNING,MULTICAST,IPv4,VIRTUAL> mtu 8232 index 1
+        inet 127.0.0.1 netmask ff000000
+net0: flags=100001100943<UP,BROADCAST,RUNNING,PROMISC,MULTICAST,ROUTER,IPv4,PHYSRUNNING> mtu 1500 index 2
+        inet 10.10.10.38 netmask ffffffe0 broadcast 10.10.10.63
+ilbint0: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 3
+        inet 10.6.0.11 netmask ffffff00 broadcast 10.6.0.255
+ilbext0: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 4
+        inet 10.10.11.11 netmask ffffffe0 broadcast 10.10.11.31
+ilbext0:1: flags=110001100843<UP,BROADCAST,RUNNING,MULTICAST,ROUTER,IPv4,VRRP,PHYSRUNNING> mtu 1500 index 4
+        inet 10.10.11.12 netmask ffffffe0 broadcast 10.10.11.31
+vpn0: flags=1000011008d1<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST,ROUTER,IPv4,PHYSRUNNING> mtu 1480 index 5
+        inet tunnel src 10.10.11.12 tunnel dst 10.10.5.5
+        tunnel hop limit 64
+        inet 10.6.0.14 --> 10.6.0.15 netmask ff000000
+lo0: flags=2002000849<UP,LOOPBACK,RUNNING,MULTICAST,IPv6,VIRTUAL> mtu 8252 index 1
+        inet6 ::1/128
+net0: flags=120002004941<UP,RUNNING,PROMISC,MULTICAST,DHCP,IPv6,PHYSRUNNING> mtu 1500 index 2
+        inet6 fe80::221:9bff:fefd:2a22/10
+ilbint0: flags=120002000840<RUNNING,MULTICAST,IPv6,PHYSRUNNING> mtu 1500 index 3
+        inet6 ::/0
+ilbext0: flags=120002000840<RUNNING,MULTICAST,IPv6,PHYSRUNNING> mtu 1500 index 4
+        inet6 ::/0
+vpn0: flags=120002200850<POINTOPOINT,RUNNING,MULTICAST,NONUD,IPv6,PHYSRUNNING> mtu 1480 index 5
+        inet tunnel src 10.10.11.12 tunnel dst 10.10.5.5
+        tunnel hop limit 64
+        inet6 ::/0 --> fe80::b2d6:7c10
+'''
+
+FREEBSD_SOCKSTAT = '''\
+USER    COMMAND     PID     FD  PROTO  LOCAL ADDRESS    FOREIGN ADDRESS
+root    python2.7   1294    41  tcp4   127.0.0.1:61115  127.0.0.1:4506
+'''
+
+
+@skipIf(NO_MOCK, NO_MOCK_REASON)
+class NetworkTestCase(TestCase):
+
+    def test_interfaces_ifconfig_linux(self):
+        interfaces = network._interfaces_ifconfig(LINUX)
+        self.assertEqual(interfaces,
+                         {'eth0': {'hwaddr': 'e0:3f:49:85:6a:af',
+                                   'inet': [{'address': '10.10.10.56',
+                                             'broadcast': '10.10.10.255',
+                                             'netmask': '255.255.252.0'}],
+                                   'inet6': [{'address': 'fe80::e23f:49ff:fe85:6aaf',
+                                              'prefixlen': '64',
+                                              'scope': 'link'}],
+                                   'up': True},
+                          'lo': {'inet': [{'address': '127.0.0.1',
+                                           'netmask': '255.0.0.0'}],
+                                 'inet6': [{'address': '::1',
+                                            'prefixlen': '128',
+                                            'scope': 'host'}],
+                                 'up': True}}
+        )
+
+    def test_interfaces_ifconfig_freebsd(self):
+        interfaces = network._interfaces_ifconfig(FREEBSD)
+        self.assertEqual(interfaces,
+                         {'': {'up': False},
+                          'em0': {'hwaddr': '00:30:48:ff:ff:ff',
+                                  'inet': [{'address': '10.10.10.250',
+                                            'broadcast': '10.10.10.255',
+                                            'netmask': '255.255.255.224'},
+                                           {'address': '10.10.10.56',
+                                            'broadcast': '10.10.10.63',
+                                            'netmask': '255.255.255.192'}],
+                                  'up': True},
+                          'em1': {'hwaddr': '00:30:48:aa:aa:aa',
+                                  'up': False},
+                          'lo0': {'inet': [{'address': '127.0.0.1',
+                                            'netmask': '255.0.0.0'}],
+                                  'inet6': [{'address': 'fe80::1',
+                                             'prefixlen': '64',
+                                             'scope': '0x8'},
+                                            {'address': '::1',
+                                             'prefixlen': '128',
+                                             'scope': None}],
+                                  'up': True},
+                          'plip0': {'up': False},
+                          'tun0': {'inet': [{'address': '10.12.0.1',
+                                             'netmask': '255.255.255.255'}],
+                                   'up': True}}
+
+        )
+
+    def test_interfaces_ifconfig_solaris(self):
+        with patch('salt.utils.is_sunos', lambda: True):
+            interfaces = network._interfaces_ifconfig(SOLARIS)
+            expected_interfaces = {'ilbint0':
+                                       {'inet6': [],
+                                        'inet': [{'broadcast': '10.6.0.255',
+                                                  'netmask': '255.255.255.0',
+                                                  'address': '10.6.0.11'}],
+                                        'up': True},
+                                   'lo0':
+                                       {'inet6': [{'prefixlen': '128',
+                                                   'address': '::1'}],
+                                       'inet': [{'netmask': '255.0.0.0',
+                                                 'address': '127.0.0.1'}],
+                                        'up': True},
+                                   'ilbext0': {'inet6': [],
+                                               'inet': [{'broadcast': '10.10.11.31',
+                                                         'netmask': '255.255.255.224',
+                                                         'address': '10.10.11.11'},
+                                                        {'broadcast': '10.10.11.31',
+                                                         'netmask': '255.255.255.224',
+                                                         'address': '10.10.11.12'}],
+                                               'up': True},
+                                   'vpn0': {'inet6': [],
+                                            'inet': [{'netmask': '255.0.0.0',
+                                                      'address': '10.6.0.14'}],
+                                            'up': True},
+                                   'net0': {'inet6': [{'prefixlen': '10',
+                                                       'address': 'fe80::221:9bff:fefd:2a22'}],
+                                   'inet': [{'broadcast': '10.10.10.63',
+                                             'netmask': '255.255.255.224',
+                                             'address': '10.10.10.38'}],
+                                            'up': True}}
+            self.assertEqual(interfaces, expected_interfaces)
+
+    def test_freebsd_remotes_on(self):
+        with patch('salt.utils.is_sunos', lambda: False):
+            with patch('salt.utils.is_freebsd', lambda: True):
+                with patch('subprocess.check_output',
+                           return_value=FREEBSD_SOCKSTAT):
+                    remotes = network._freebsd_remotes_on('4506', 'remote')
+                    self.assertEqual(remotes, set(['127.0.0.1']))
+
+    @patch('platform.node', MagicMock(return_value='nodename'))
+    @patch('socket.gethostname', MagicMock(return_value='hostname'))
+    @patch('socket.getfqdn', MagicMock(return_value='hostname.domainname.blank'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'attrname', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '5.6.7.8']))
+    def test_generate_minion_id_distinct(self):
+        '''
+        Test if minion IDs are distinct in the pool.
+
+        :return:
+        '''
+        self.assertEqual(network._generate_minion_id(),
+                         ['hostname.domainname.blank', 'nodename', 'hostname', '1.2.3.4', '5.6.7.8'])
+
+    @patch('platform.node', MagicMock(return_value='hostname'))
+    @patch('socket.gethostname', MagicMock(return_value='hostname'))
+    @patch('socket.getfqdn', MagicMock(return_value='hostname'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'hostname', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '1.2.3.4', '1.2.3.4']))
+    def test_generate_minion_id_duplicate(self):
+        '''
+        Test if IP addresses in the minion IDs are distinct in the pool
+
+        :return:
+        '''
+        self.assertEqual(network._generate_minion_id(), ['hostname', '1.2.3.4'])
+
+    @patch('platform.node', MagicMock(return_value='very.long.and.complex.domain.name'))
+    @patch('socket.gethostname', MagicMock(return_value='hostname'))
+    @patch('socket.getfqdn', MagicMock(return_value=''))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'hostname', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '1.2.3.4', '1.2.3.4']))
+    def test_generate_minion_id_platform_used(self):
+        '''
+        Test if platform.node is used for the first occurrence.
+        The platform.node is most common hostname resolver before anything else.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), 'very.long.and.complex.domain.name')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='pick.me'))
+    @patch('socket.getfqdn', MagicMock(return_value='hostname.domainname.blank'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'hostname', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '1.2.3.4', '1.2.3.4']))
+    def test_generate_minion_id_platform_localhost_filtered(self):
+        '''
+        Test if localhost is filtered from the first occurrence.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), 'hostname.domainname.blank')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='ip6-loopback'))
+    @patch('socket.getfqdn', MagicMock(return_value='ip6-localhost'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'localhost', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1', '1.2.3.4']))
+    def test_generate_minion_id_platform_localhost_filtered_all(self):
+        '''
+        Test if any of the localhost is filtered from everywhere.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), '1.2.3.4')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='ip6-loopback'))
+    @patch('socket.getfqdn', MagicMock(return_value='ip6-localhost'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'localhost', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1']))
+    def test_generate_minion_id_platform_localhost_only(self):
+        '''
+        Test if there is no other choice but localhost.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), 'localhost')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='ip6-loopback'))
+    @patch('socket.getfqdn', MagicMock(return_value='pick.me'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'localhost', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1']))
+    def test_generate_minion_id_platform_fqdn(self):
+        '''
+        Test if fqdn is picked up.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), 'pick.me')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='ip6-loopback'))
+    @patch('socket.getfqdn', MagicMock(return_value='ip6-localhost'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'pick.me', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1']))
+    def test_generate_minion_id_platform_localhost_addrinfo(self):
+        '''
+        Test if addinfo is picked up.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), 'pick.me')
+
+    @patch('platform.node', MagicMock(return_value='localhost'))
+    @patch('socket.gethostname', MagicMock(return_value='ip6-loopback'))
+    @patch('socket.getfqdn', MagicMock(return_value='ip6-localhost'))
+    @patch('socket.getaddrinfo', MagicMock(return_value=[(2, 3, 0, 'localhost', ('127.0.1.1', 0))]))
+    @patch('salt.utils.fopen', MagicMock(return_valute=False))
+    @patch('os.path.exists', MagicMock(return_valute=False))
+    @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1', '1.2.3.4']))
+    def test_generate_minion_id_platform_ip_addr_only(self):
+        '''
+        Test if IP address is the only what is used as a Minion ID in case no DNS name.
+
+        :return:
+        '''
+        self.assertEqual(network.generate_minion_id(), '1.2.3.4')
+
+
+if __name__ == '__main__':
+    from integration import run_tests
+    run_tests(NetworkTestCase, needs_daemon=False)
-- 
2.10.2

