undercloud: resolve undercloud_*_host
The problem we're solving here is that our operators using SSL + FQDN
based endpoints will have failures during the deployment because we
don't lookup the FQDN into IP addresses, needed later in the deployment
for proper binding.
This patch transforms undercloud_*_host parameters into IP addresses:
- We raise if lookup returns nothing.
- We raise if lookup returns more than one IP.
- We support both IPv4 and IPv6.
- We raise if the IP is a loopback.
- We raise if the returned IP is invalid.
Utils changes:
* Introduce utils.is_valid_ip.
Return True if the IP is either v4 or v6. Return False otherwise.
* Introduce utils.is_loopback.
Return True if the given host is a loopback. Return False otherwise.
* Introduce utils.get_host_ips.
Returns a list of IPs for a host to lookup.
* Introduce utils.get_single_ip.
Translate an hostname or FQDN into an IP address if it is valid IP.
Return it unchanged if it is an IPv4 or IPv6 address.
If the host is not reachable, it'll raise an exception.
By default it excludes the loopbacks but it can be allowed by setting
allow_loopback = True.
* Use utils.get_single_ip to translate undercloud_admin_host and
undercloud_public_host to IP addresses.
Related-Bug: #1763776
Change-Id: Ic008cc758493aa95e8aa237d23c2f66c0a930509
(cherry picked from commit c925c3b93b
)
This commit is contained in:
parent
ff5e427633
commit
098fdb2cd9
|
@ -41,6 +41,10 @@ class NotFound(Base):
|
|||
"""Resource not found"""
|
||||
|
||||
|
||||
class LookupError(Base):
|
||||
"""Lookup Error"""
|
||||
|
||||
|
||||
class DeploymentError(Base):
|
||||
"""Deployment failed"""
|
||||
|
||||
|
|
|
@ -888,6 +888,70 @@ class TestBracketIPV6(TestCase):
|
|||
self.assertEqual('[::1]', result)
|
||||
|
||||
|
||||
class TestIsValidIP(TestCase):
|
||||
def test_with_valid_ipv4(self):
|
||||
result = utils.is_valid_ip('192.168.0.1')
|
||||
self.assertEqual(True, result)
|
||||
|
||||
def test_with_valid_ipv6(self):
|
||||
result = utils.is_valid_ip('::1')
|
||||
self.assertEqual(True, result)
|
||||
|
||||
def test_with_invalid_ip(self):
|
||||
result = utils.is_valid_ip('192.168.1%bad')
|
||||
self.assertEqual(False, result)
|
||||
|
||||
|
||||
class TestIsLoopback(TestCase):
|
||||
def test_with_loopback(self):
|
||||
result = utils.is_loopback('127.0.0.1')
|
||||
self.assertEqual(True, result)
|
||||
|
||||
def test_with_no_loopback(self):
|
||||
result = utils.is_loopback('10.0.0.1')
|
||||
self.assertEqual(False, result)
|
||||
|
||||
|
||||
class TestGetHostIps(TestCase):
|
||||
def test_get_host_ips(self):
|
||||
with mock.patch.object(socket, 'getaddrinfo') as mock_addrinfo:
|
||||
mock_addrinfo.return_value = [('', '', 6, '', ('127.0.0.1', 0))]
|
||||
result = utils.get_host_ips('myhost.domain')
|
||||
self.assertEqual(['127.0.0.1'], result)
|
||||
|
||||
|
||||
class TestGetSingleIp(TestCase):
|
||||
def test_with_fqdn_and_valid_ip(self):
|
||||
with mock.patch.object(utils, 'get_host_ips') as mock_gethostips:
|
||||
mock_gethostips.return_value = ['192.168.0.1']
|
||||
result = utils.get_single_ip('myhost.domain')
|
||||
self.assertEqual('192.168.0.1', result)
|
||||
|
||||
def test_with_fqdn_and_loopback(self):
|
||||
with mock.patch.object(utils, 'get_host_ips') as mock_gethostips:
|
||||
mock_gethostips.return_value = ['127.0.0.1']
|
||||
self.assertRaises(exceptions.LookupError,
|
||||
utils.get_single_ip, 'myhost.domain')
|
||||
|
||||
def test_with_too_much_ips(self):
|
||||
with mock.patch.object(utils, 'get_host_ips') as mock_gethostips:
|
||||
mock_gethostips.return_value = ['192.168.0.1', '192.168.0.2']
|
||||
self.assertRaises(exceptions.LookupError,
|
||||
utils.get_single_ip, 'myhost.domain')
|
||||
|
||||
def test_without_ip(self):
|
||||
with mock.patch.object(utils, 'get_host_ips') as mock_gethostips:
|
||||
mock_gethostips.return_value = []
|
||||
self.assertRaises(exceptions.LookupError,
|
||||
utils.get_single_ip, 'myhost.domain')
|
||||
|
||||
def test_with_invalid_ip(self):
|
||||
with mock.patch.object(utils, 'get_host_ips') as mock_gethostips:
|
||||
mock_gethostips.return_value = ['192.168.23.x']
|
||||
self.assertRaises(exceptions.LookupError,
|
||||
utils.get_single_ip, 'myhost.domain')
|
||||
|
||||
|
||||
class TestStoreCliParam(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -24,6 +24,7 @@ import shutil
|
|||
from six.moves.configparser import ConfigParser
|
||||
|
||||
import json
|
||||
import netaddr
|
||||
import os
|
||||
import os.path
|
||||
import simplejson
|
||||
|
@ -152,6 +153,84 @@ def bracket_ipv6(address):
|
|||
return address
|
||||
|
||||
|
||||
def is_valid_ip(ip):
|
||||
"""Return True if the IP is either v4 or v6
|
||||
|
||||
Return False if invalid.
|
||||
"""
|
||||
return netaddr.valid_ipv4(ip) or netaddr.valid_ipv6(ip)
|
||||
|
||||
|
||||
def is_loopback(host):
|
||||
"""Return True of the IP or the host is a loopback
|
||||
|
||||
Return False if not.
|
||||
"""
|
||||
loopbacks = ['127', '::1']
|
||||
for l in loopbacks:
|
||||
if host.startswith(l):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_host_ips(host, type=None):
|
||||
"""Lookup an host to return a list of IPs.
|
||||
|
||||
:param host: Host to lookup
|
||||
:type host: string
|
||||
|
||||
:param type: Type of socket (e.g. socket.AF_INET, socket.AF_INET6)
|
||||
:type type: string
|
||||
"""
|
||||
|
||||
ips = set()
|
||||
if type:
|
||||
types = (type,)
|
||||
else:
|
||||
types = (socket.AF_INET, socket.AF_INET6)
|
||||
for t in types:
|
||||
try:
|
||||
res = socket.getaddrinfo(host, None, t, socket.SOCK_STREAM)
|
||||
except socket.error:
|
||||
continue
|
||||
nips = set([x[4][0] for x in res])
|
||||
ips.update(nips)
|
||||
return list(ips)
|
||||
|
||||
|
||||
def get_single_ip(host, allow_loopback=False):
|
||||
"""Translate an hostname into a single IP address if it is a valid IP.
|
||||
|
||||
:param host: IP or hostname or FQDN to lookup
|
||||
:type host: string
|
||||
|
||||
:param allow_loopback: Whether or not a loopback IP can be returned.
|
||||
Defaults is False.
|
||||
:type allow_loopback: boolean
|
||||
|
||||
Return the host unchanged if it is already an IPv4 or IPv6 address.
|
||||
"""
|
||||
|
||||
ip = host
|
||||
if not is_valid_ip(host):
|
||||
ips = get_host_ips(host)
|
||||
if not ips:
|
||||
raise exceptions.LookupError('No IP was found for the host: '
|
||||
'%s' % host)
|
||||
else:
|
||||
ip = ips[0]
|
||||
if len(ips) > 1:
|
||||
raise exceptions.LookupError('More than one IP was found for the '
|
||||
'host %s: %s' % (host, ips))
|
||||
if not allow_loopback and is_loopback(ip):
|
||||
raise exceptions.LookupError('IP address for host %s is a loopback'
|
||||
' IP: %s' % (host, ip))
|
||||
if not is_valid_ip(ip):
|
||||
raise exceptions.LookupError('IP address for host %s is not a '
|
||||
'valid IP: %s' % (host, ip))
|
||||
return ip
|
||||
|
||||
|
||||
def write_env_file(env_data, env_file, registry_overwrites):
|
||||
"""Write the tht env file as yaml"""
|
||||
|
||||
|
|
|
@ -306,8 +306,10 @@ def _calculate_allocation_pools(subnet):
|
|||
# Remove gateway, local_ip, admin_host and public_host addresses
|
||||
ip_set.remove(netaddr.IPAddress(subnet.get('gateway')))
|
||||
ip_set.remove(netaddr.IPNetwork(CONF.local_ip).ip)
|
||||
ip_set.remove(netaddr.IPNetwork(CONF.undercloud_admin_host))
|
||||
ip_set.remove(netaddr.IPNetwork(CONF.undercloud_public_host))
|
||||
ip_set.remove(netaddr.IPNetwork(utils.get_single_ip(
|
||||
CONF.undercloud_admin_host)))
|
||||
ip_set.remove(netaddr.IPNetwork(utils.get_single_ip(
|
||||
CONF.undercloud_public_host)))
|
||||
# Remove addresses in the inspection_iprange
|
||||
inspect_start, inspect_end = subnet.get('inspection_iprange').split(',')
|
||||
ip_set.remove(netaddr.IPRange(inspect_start, inspect_end))
|
||||
|
@ -581,18 +583,19 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False,
|
|||
CONF.get('undercloud_service_certificate')):
|
||||
endpoint_environment = _get_tls_endpoint_environment(
|
||||
CONF.get('undercloud_public_host'), tht_templates)
|
||||
try:
|
||||
public_host = CONF.get('undercloud_public_host')
|
||||
netaddr.IPAddress(public_host)
|
||||
deploy_args += ['--public-virtual-ip', public_host]
|
||||
|
||||
admin_host = CONF.get('undercloud_admin_host')
|
||||
netaddr.IPAddress(admin_host)
|
||||
deploy_args += ['--control-virtual-ip', admin_host]
|
||||
except netaddr.core.AddrFormatError:
|
||||
# TODO(jaosorior): We could do a reverse lookup for the hostnames
|
||||
# if the *_host variables are DNS names and not IPs.
|
||||
pass
|
||||
public_host = utils.get_single_ip(CONF.get('undercloud_public_host'))
|
||||
netaddr.IPAddress(public_host)
|
||||
deploy_args += ['--public-virtual-ip', public_host]
|
||||
|
||||
# To make sure the resolved host is set to the right IP in /etc/hosts
|
||||
if not utils.is_valid_ip(CONF.get('undercloud_public_host')):
|
||||
extra_host = public_host + ' ' + CONF.get('undercloud_public_host')
|
||||
env_data['ExtraHostFileEntries'] = extra_host
|
||||
|
||||
admin_host = utils.get_single_ip(CONF.get('undercloud_admin_host'))
|
||||
netaddr.IPAddress(admin_host)
|
||||
deploy_args += ['--control-virtual-ip', admin_host]
|
||||
|
||||
deploy_args += [
|
||||
'-e', endpoint_environment,
|
||||
|
|
Loading…
Reference in New Issue