mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
fix minion id generation
This commit is contained in:
parent
4ba9314aca
commit
d4b7951da8
3 changed files with 244 additions and 137 deletions
104
salt/config.py
104
salt/config.py
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue