Add get_server_external_ipv4() to meta
It turned out we can't rely on get_server_public_ip to get an externally reachable IP of servers because not always floating IPs are used and not always are what the caller is looking for. get_server_external_ipv4() tries hard to provide an IP address of the given server that can be reached from networks that are external to the cloud. Unfortunately from some clouds (or for very particularly configured servers with multiple interfaces) it could not be possible to reliably determine those IP addresses. Those clouds are corner cases that are not supported by get_server_external_ipv4() Co-Authored-By: Monty Taylor <mordred@inaugust.com> Change-Id: I099193437d0f45cfda78923349e500e9b2e0e053
This commit is contained in:
parent
693c76fc87
commit
07f72e19f5
|
@ -12,8 +12,12 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from socket import inet_aton
|
||||
from struct import unpack
|
||||
|
||||
from shade import exc
|
||||
|
||||
|
||||
|
@ -153,3 +157,39 @@ def normalize_nova_secgroup_rules(rules):
|
|||
'remote_ip_prefix': r['ip_range'].get('cidr', None),
|
||||
'security_group_id': r['parent_group_id']
|
||||
} for r in rules]
|
||||
|
||||
|
||||
def is_ipv4(ip):
|
||||
return re.match(
|
||||
'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|'
|
||||
'[01]?[0-9][0-9]?)$', ip) is not None
|
||||
|
||||
|
||||
def is_globally_routable_ipv4(ip):
|
||||
# Comprehensive list of non-globally routable IPv4 networks
|
||||
ngr_nets = (
|
||||
["192.168.0.0", "255.255.0.0"], # rfc1918
|
||||
["172.16.0.0", "255.240.0.0"], # rfc1918
|
||||
["10.0.0.0", "255.0.0.0"], # rfc1918
|
||||
["192.0.2.0", "255.255.255.0"], # rfc5737
|
||||
["198.51.100.0", "255.255.255.0"], # rfc5737
|
||||
["203.0.113.0", "255.255.255.0"], # rfc5737
|
||||
["169.254.0.0", "255.255.0.0"], # rfc3927
|
||||
["100.64.0.0", "255.192.0.0"], # rfc6598
|
||||
["192.0.0.0", "255.255.255.0"], # rfc5736
|
||||
["192.88.99.0", "255.255.255.0"], # rfc3068
|
||||
["198.18.0.0", "255.254.0.0"], # rfc2544
|
||||
["224.0.0.0", "240.0.0.0"], # rfc5771
|
||||
["240.0.0.0", "240.0.0.0"], # rfc6890
|
||||
["0.0.0.0", "255.0.0.0"], # rfc1700
|
||||
["255.255.255.255", "0.0.0.0"], # rfc6890
|
||||
["127.0.0.0", "255.0.0.0"], # rfc3330
|
||||
)
|
||||
|
||||
int_ip = unpack('!I', inet_aton(ip))[0]
|
||||
for net in ngr_nets:
|
||||
mask = unpack('!I', inet_aton(net[1]))[0]
|
||||
if (int_ip & mask) == unpack('!I', inet_aton(net[0]))[0]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
@ -14,12 +14,19 @@
|
|||
|
||||
|
||||
import bunch
|
||||
import logging
|
||||
import six
|
||||
|
||||
from neutronclient.common.exceptions import NeutronClientException
|
||||
|
||||
from shade import exc
|
||||
from shade import _utils
|
||||
|
||||
|
||||
NON_CALLABLES = (six.string_types, bool, dict, int, list, type(None))
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4):
|
||||
|
||||
|
@ -52,6 +59,55 @@ def get_server_public_ip(server):
|
|||
return get_server_ip(server, ext_tag='floating', key_name='public')
|
||||
|
||||
|
||||
def get_server_external_ipv4(cloud, server):
|
||||
if cloud.has_service('network'):
|
||||
try:
|
||||
# Search a fixed IP attached to an external net. Unfortunately
|
||||
# Neutron ports don't have a 'floating_ips' attribute
|
||||
server_ports = cloud.search_ports(
|
||||
filters={'device_id': server.id})
|
||||
ext_nets = cloud.search_networks(filters={'router:external': True})
|
||||
except NeutronClientException as e:
|
||||
log.debug(
|
||||
"Something went wrong talking to neutron API: "
|
||||
"'{msg}'. Trying with Nova.".format(msg=str(e)))
|
||||
# Fall-through, trying with Nova
|
||||
else:
|
||||
for net in ext_nets:
|
||||
for port in server_ports:
|
||||
if net['id'] == port['network_id']:
|
||||
for ip in port['fixed_ips']:
|
||||
if _utils.is_ipv4(ip['ip_address']):
|
||||
return ip['ip_address']
|
||||
# The server doesn't have an interface on an external network so it
|
||||
# can either have a floating IP or have no way to be reached from
|
||||
# outside the cloud.
|
||||
# Fall-through, trying with Nova
|
||||
|
||||
# The cloud doesn't support Neutron or Neutron can't be contacted. The
|
||||
# server might have fixed addresses that are reachable from outside the
|
||||
# cloud (e.g. Rax) or have plain ol' floating IPs
|
||||
|
||||
# Try to get an address from a network named 'public'
|
||||
ext_ip = get_server_ip(server, key_name='public')
|
||||
if ext_ip is not None:
|
||||
return ext_ip
|
||||
|
||||
# Try to find a globally routable IP address
|
||||
for interfaces in server.addresses.values():
|
||||
for interface in interfaces:
|
||||
if _utils.is_ipv4(interface['addr']) and \
|
||||
_utils.is_globally_routable_ipv4(interface['addr']):
|
||||
return interface['addr']
|
||||
|
||||
# Last, try to get a floating IP address
|
||||
ext_ip = get_server_ip(server, ext_tag='floating')
|
||||
if ext_ip is not None:
|
||||
return ext_ip
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_groups_from_server(cloud, server, server_vars):
|
||||
groups = []
|
||||
|
||||
|
|
|
@ -56,10 +56,11 @@ class FakeProject(object):
|
|||
|
||||
|
||||
class FakeServer(object):
|
||||
def __init__(self, id, name, status):
|
||||
def __init__(self, id, name, status, addresses=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.addresses = addresses
|
||||
|
||||
|
||||
class FakeService(object):
|
||||
|
|
|
@ -16,7 +16,12 @@ import mock
|
|||
import testtools
|
||||
import warlock
|
||||
|
||||
from neutronclient.common import exceptions as neutron_exceptions
|
||||
|
||||
import shade
|
||||
from shade import meta
|
||||
from shade import _utils
|
||||
from shade.tests import fakes
|
||||
|
||||
PRIVATE_V4 = '198.51.100.3'
|
||||
PUBLIC_V4 = '192.0.2.99'
|
||||
|
@ -78,6 +83,90 @@ class TestMeta(testtools.TestCase):
|
|||
self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv))
|
||||
self.assertEqual(PUBLIC_V4, meta.get_server_public_ip(srv))
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'has_service')
|
||||
@mock.patch.object(shade.OpenStackCloud, 'search_ports')
|
||||
@mock.patch.object(shade.OpenStackCloud, 'search_networks')
|
||||
@mock.patch.object(meta, 'get_server_ip')
|
||||
def test_get_server_external_ipv4_neutron(
|
||||
self, mock_get_server_ip, mock_search_networks,
|
||||
mock_search_ports, mock_has_service):
|
||||
# Testing Clouds with Neutron
|
||||
mock_has_service.return_value = True
|
||||
mock_search_ports.return_value = [{
|
||||
'network_id': 'test-net-id',
|
||||
'fixed_ips': [{'ip_address': PUBLIC_V4}],
|
||||
'device_id': 'test-id'
|
||||
}]
|
||||
mock_search_networks.return_value = [{'id': 'test-net-id'}]
|
||||
|
||||
srv = fakes.FakeServer(
|
||||
id='test-id', name='test-name', status='ACTIVE')
|
||||
ip = meta.get_server_external_ipv4(
|
||||
cloud=shade.openstack_cloud(), server=srv)
|
||||
|
||||
self.assertEqual(PUBLIC_V4, ip)
|
||||
self.assertFalse(mock_get_server_ip.called)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'has_service')
|
||||
@mock.patch.object(shade.OpenStackCloud, 'search_ports')
|
||||
@mock.patch.object(meta, 'get_server_ip')
|
||||
def test_get_server_external_ipv4_neutron_exception(
|
||||
self, mock_get_server_ip, mock_search_ports, mock_has_service):
|
||||
# Testing Clouds with a non working Neutron
|
||||
mock_has_service.return_value = True
|
||||
mock_search_ports.side_effect = neutron_exceptions.NotFound()
|
||||
mock_get_server_ip.return_value = PUBLIC_V4
|
||||
|
||||
srv = fakes.FakeServer(
|
||||
id='test-id', name='test-name', status='ACTIVE')
|
||||
ip = meta.get_server_external_ipv4(
|
||||
cloud=shade.openstack_cloud(), server=srv)
|
||||
|
||||
self.assertEqual(PUBLIC_V4, ip)
|
||||
self.assertTrue(mock_get_server_ip.called)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'has_service')
|
||||
@mock.patch.object(meta, 'get_server_ip')
|
||||
@mock.patch.object(_utils, 'is_globally_routable_ipv4')
|
||||
def test_get_server_external_ipv4_nova_public(
|
||||
self, mock_is_globally_routable_ipv4,
|
||||
mock_get_server_ip, mock_has_service):
|
||||
# Testing Clouds w/o Neutron and a network named public
|
||||
mock_has_service.return_value = False
|
||||
mock_get_server_ip.return_value = None
|
||||
mock_is_globally_routable_ipv4.return_value = True
|
||||
|
||||
srv = fakes.FakeServer(
|
||||
id='test-id', name='test-name', status='ACTIVE',
|
||||
addresses={'test-net': [{'addr': PUBLIC_V4}]})
|
||||
ip = meta.get_server_external_ipv4(
|
||||
cloud=shade.openstack_cloud(), server=srv)
|
||||
|
||||
self.assertEqual(PUBLIC_V4, ip)
|
||||
self.assertTrue(mock_get_server_ip.called)
|
||||
self.assertTrue(mock_is_globally_routable_ipv4.called)
|
||||
|
||||
@mock.patch.object(shade.OpenStackCloud, 'has_service')
|
||||
@mock.patch.object(meta, 'get_server_ip')
|
||||
@mock.patch.object(_utils, 'is_globally_routable_ipv4')
|
||||
def test_get_server_external_ipv4_nova_none(
|
||||
self, mock_is_globally_routable_ipv4,
|
||||
mock_get_server_ip, mock_has_service):
|
||||
# Testing Clouds w/o Neutron and a globally routable IP
|
||||
mock_has_service.return_value = False
|
||||
mock_get_server_ip.return_value = None
|
||||
mock_is_globally_routable_ipv4.return_value = False
|
||||
|
||||
srv = fakes.FakeServer(
|
||||
id='test-id', name='test-name', status='ACTIVE',
|
||||
addresses={'test-net': [{'addr': PRIVATE_V4}]})
|
||||
ip = meta.get_server_external_ipv4(
|
||||
cloud=shade.openstack_cloud(), server=srv)
|
||||
|
||||
self.assertIsNone(ip)
|
||||
self.assertTrue(mock_get_server_ip.called)
|
||||
self.assertTrue(mock_is_globally_routable_ipv4.called)
|
||||
|
||||
def test_get_groups_from_server(self):
|
||||
server_vars = {'flavor': 'test-flavor',
|
||||
'image': 'test-image',
|
||||
|
|
Loading…
Reference in New Issue