diff --git a/tripleoclient/exceptions.py b/tripleoclient/exceptions.py index 937a375d2..70f861297 100644 --- a/tripleoclient/exceptions.py +++ b/tripleoclient/exceptions.py @@ -41,6 +41,10 @@ class NotFound(Base): """Resource not found""" +class LookupError(Base): + """Lookup Error""" + + class DeploymentError(Base): """Deployment failed""" diff --git a/tripleoclient/tests/test_utils.py b/tripleoclient/tests/test_utils.py index 39ab427e9..8be07ace9 100644 --- a/tripleoclient/tests/test_utils.py +++ b/tripleoclient/tests/test_utils.py @@ -956,6 +956,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): diff --git a/tripleoclient/utils.py b/tripleoclient/utils.py index 7e4f3af81..ecefe8a21 100644 --- a/tripleoclient/utils.py +++ b/tripleoclient/utils.py @@ -24,6 +24,7 @@ import shutil from six.moves.configparser import ConfigParser import json +import netaddr import os import os.path import simplejson @@ -171,6 +172,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""" diff --git a/tripleoclient/v1/undercloud_config.py b/tripleoclient/v1/undercloud_config.py index e6a706da0..06fc7ff9a 100644 --- a/tripleoclient/v1/undercloud_config.py +++ b/tripleoclient/v1/undercloud_config.py @@ -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,