fix minion id generation

This commit is contained in:
s8weber 2014-05-02 22:24:13 -04:00
parent 4ba9314aca
commit d4b7951da8
3 changed files with 244 additions and 137 deletions

View file

@ -1723,16 +1723,9 @@ def get_id(root_dir=None, minion_id=False, cache=True):
'''
Guess the id of the minion.
- If CONFIG_DIR/minion_id exists, use the cached minion ID from that file
- If salt.utils.network.get_fqhostname returns us something other than
localhost, use it
- Check /etc/hostname for a value other than localhost
- Check /etc/hosts for something that isn't localhost that maps to 127.*
- Look for a routeable / public IP
- A private IP is better than a loopback IP
- localhost may be better than killing the minion
Any non-ip id will be cached for later use in ``CONFIG_DIR/minion_id``
If CONFIG_DIR/minion_id exists, use the cached minion ID from that file.
If no minion id is configured, use multiple sources to find a FQDN.
If no FQDN is found you may get an ip address.
Returns two values: the detected ID, and a boolean value noting whether or
not an IP address is being used for the ID.
@ -1763,91 +1756,12 @@ def get_id(root_dir=None, minion_id=False, cache=True):
log.debug('Guessing ID. The id can be explicitly in set {0}'
.format(os.path.join(salt.syspaths.CONFIG_DIR, 'minion')))
# Check salt.utils.network.get_fqhostname()
fqdn = salt.utils.network.get_fqhostname()
if fqdn != 'localhost':
log.info('Found minion id from get_fqhostname(): {0}'.format(fqdn))
if minion_id and cache:
_cache_id(fqdn, id_cache)
return fqdn, False
# Check /etc/hostname
try:
with salt.utils.fopen('/etc/hostname') as hfl:
name = hfl.read().strip()
if re.search(r'\s', name):
log.warning('Whitespace character detected in /etc/hostname. '
'This file should not contain any whitespace.')
else:
if name != 'localhost':
if minion_id and cache:
_cache_id(name, id_cache)
return name, False
except (IOError, OSError):
pass
# Can /etc/hosts help us?
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.'):
for name in names:
if name != 'localhost':
log.info('Found minion id in hosts file: {0}'
.format(name))
if minion_id and cache:
_cache_id(name, id_cache)
return name, False
except (IOError, OSError):
pass
if salt.utils.is_windows():
# Can Windows 'hosts' file help?
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
if name != 'localhost':
log.info('Found minion id in hosts file: {0}'
.format(name))
if minion_id and cache:
_cache_id(name, id_cache)
return name, False
except IndexError:
pass # could not split line (malformed entry?)
except (IOError, OSError):
pass
# What IP addresses do we have?
ip_addresses = [salt.utils.network.IPv4Address(addr) for addr
in salt.utils.network.ip_addrs(include_loopback=True)
if not addr.startswith('127.')]
for addr in ip_addresses:
if not addr.is_private:
log.info('Using public ip address for id: {0}'.format(addr))
return str(addr), True
if ip_addresses:
addr = ip_addresses.pop(0)
log.info('Using private ip address for id: {0}'.format(addr))
return str(addr), True
log.error('No id found, falling back to localhost')
return 'localhost', False
newid = salt.utils.network.generate_minion_id()
log.info('Found minion id from generate_minion_id(): {0}'.format(newid))
if minion_id and cache:
_cache_id(newid, id_cache)
is_ipv4 = newid.count('.') == 3 and not any(c.isalpha() for c in newid)
return newid, is_ipv4
def apply_minion_config(overrides=None,

View file

@ -77,6 +77,206 @@ 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::.*',
]
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',
'127.0.2.1',
'127.0.1.1',
'127.0.0.1',
'0.0.0.0',
'::1',
'fe00::',
'fe02::',
]
def _cmp_hostname(a, b):
# should never have a space in hostname
if ' ' in a:
return 1
if ' ' in b:
return -1
# punish localhost list
if a in punish:
if b in punish:
return punish.index(a) - punish.index(b)
return 1
if b in punish:
return -1
# punish ipv6
if ':' in a or ':' in b:
return a.count(':') - b.count(':')
# punish ipv4
a_is_ipv4 = a.count('.') == 3 and not any(c.isalpha() for c in a)
b_is_ipv4 = b.count('.') == 3 and not any(c.isalpha() for c in b)
if a_is_ipv4 and a.startswith('127.'):
return 1
if b_is_ipv4 and b.startswith('127.'):
return -1
if a_is_ipv4 and not b_is_ipv4:
return 1
if a_is_ipv4 and b_is_ipv4:
return 0
if not a_is_ipv4 and b_is_ipv4:
return -1
# favor hosts with more dots
diff = b.count('.') - a.count('.')
if diff != 0:
return diff
# favor longest fqdn
return len(b) - len(a)
return sorted(hostname_list, cmp=_cmp_hostname)
def get_hostnames():
'''
Get list of hostnames using multiple strategies
'''
h = []
h.append(socket.gethostname())
h.append(socket.getfqdn())
# 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
# 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
# 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
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
CLI Example::
salt '*' network.generate_minion_id
'''
possible_ids = get_hostnames()
ip_addresses = [IPv4Address(addr) for addr
in salt.utils.network.ip_addrs(include_loopback=True)
if not addr.startswith('127.')]
# include public and private ipaddresses
for addr in ip_addresses:
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]
def get_fqhostname():
'''
Returns the fully qualified hostname
@ -85,30 +285,27 @@ def get_fqhostname():
salt '*' network.get_fqhostname
'''
h_name = socket.gethostname()
if h_name.find('.') >= 0:
return h_name
else:
h_fqdn = socket.getfqdn()
try:
addrinfo = socket.getaddrinfo(
h_name, 0, socket.AF_UNSPEC, socket.SOCK_STREAM,
socket.SOL_TCP, socket.AI_CANONNAME
)[0]
except IndexError:
# Handle possible empty struct returns
return h_fqdn
except socket.gaierror:
return h_fqdn
else:
# Struct contanis the following elements:
# family, socktype, proto, canonname, sockaddr
try:
# Prevent returning an empty string by falling back to
# socket.getfqdn()
return addrinfo[3] or h_fqdn
except IndexError:
return h_fqdn
l = []
l.append(socket.getfqdn())
# 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:
l.append(info[3])
except socket.gaierror:
pass
l = _sort_hostnames(l)
if len(l) > 0:
return l[0]
return None
def ip_to_host(ip):

View file

@ -35,6 +35,8 @@ from salt.version import SaltStackVersion
log = logging.getLogger(__name__)
# mock hostname should be more complex than the systems FQDN
MOCK_HOSTNAME = 'vary.long.complex.fqdn.com'
MOCK_ETC_HOSTS = (
'##\n'
@ -44,10 +46,14 @@ MOCK_ETC_HOSTS = (
'# when the system is booting. Do not change this entry.\n'
'##\n'
'\n' # This empty line MUST STAY HERE, it factors into the tests
'127.0.0.1 localhost foo.bar.net\n'
'10.0.0.100 foo.bar.net\n'
'127.0.0.1 localhost ' + MOCK_HOSTNAME + '\n'
'10.0.0.100 ' + MOCK_HOSTNAME + '\n'
'200.200.200.2 other.host.alias.com\n'
'::1 ip6-localhost ip6-loopback\n'
'fe00::0 ip6-localnet\n'
'ff00::0 ip6-mcastprefix\n'
)
MOCK_ETC_HOSTNAME = 'foo.bar.com\n'
MOCK_ETC_HOSTNAME = '{0}\n'.format(MOCK_HOSTNAME)
def _unhandled_mock_read(filename):
@ -67,6 +73,8 @@ def _fopen_side_effect_etc_hostname(filename):
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)
@ -78,7 +86,7 @@ def _fopen_side_effect_etc_hosts(filename):
'''
log.debug('Mock-reading {0}'.format(filename))
if filename == '/etc/hostname':
raise IOError(2, "No such file or directory: '/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()
@ -443,18 +451,6 @@ class ConfigTestCase(TestCase):
if os.path.isdir(tempdir):
shutil.rmtree(tempdir)
@patch('salt.utils.network.get_fqhostname', MagicMock(return_value='foo.bar.org'))
def test_get_id_socket_get_fqhostname(self):
'''
Test calling salt.config.get_id() and getting the hostname from
salt.utils.network.get_fqhostname()
'''
with patch('salt.utils.fopen',
MagicMock(side_effect=_unhandled_mock_read)):
self.assertEqual(
sconfig.get_id(cache=False), ('foo.bar.org', False)
)
@patch('salt.utils.network.get_fqhostname', MagicMock(return_value='localhost'))
def test_get_id_etc_hostname(self):
'''
@ -463,7 +459,7 @@ class ConfigTestCase(TestCase):
'''
with patch('salt.utils.fopen', _fopen_side_effect_etc_hostname):
self.assertEqual(
sconfig.get_id(cache=False), ('foo.bar.com', False)
sconfig.get_id(cache=False), (MOCK_HOSTNAME, False)
)
@patch('salt.utils.network.get_fqhostname', MagicMock(return_value='localhost'))
@ -474,7 +470,7 @@ class ConfigTestCase(TestCase):
'''
with patch('salt.utils.fopen', _fopen_side_effect_etc_hosts):
self.assertEqual(
sconfig.get_id(cache=False), ('foo.bar.net', False)
sconfig.get_id(cache=False), (MOCK_HOSTNAME, False)
)