File 0009-Rewrite-minion-ID-generator-bsc-967803.patch of Package salt.4202
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