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:
Davide Guerri 2015-06-06 17:49:02 +01:00
parent 693c76fc87
commit 07f72e19f5
4 changed files with 187 additions and 1 deletions

View File

@ -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

View File

@ -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 = []

View File

@ -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):

View File

@ -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',