Support dual-stack neutron networks

It is totally possible for a neutron network to have a network with a
globally routable IPv6 subnet and an RFC-1918 IPv4 subnet. In fact, the
existing OSIC Cloud1 does this, and the original region of Dreamhost did
this.

The trouble is, it's not possible in a reasonable way to _infer_ this
setup, so we rely on brand-new config functions in os-client-config to
allow a user to express that a network is external for ipv4 or for ipv6.

Depends-On: I40f5165d36060643943bcb91df14e5e34cd5e3fa
Change-Id: I12c491ac31b950dde4c1ac55860043fd9d05ece8
This commit is contained in:
Monty Taylor 2016-08-18 17:21:56 -05:00
parent 18aa5d1e28
commit 6832f734d0
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
7 changed files with 330 additions and 15 deletions

View File

@ -0,0 +1,8 @@
---
features:
- Added support for dual stack networks where the IPv4 subnet and the
IPv6 subnet have opposite public/private qualities. It is now possible
to add configuration to clouds.yaml that will indicate that a network
is public for v6 and private for v4, which is otherwise very difficult
to correctly infer while setting server attributes like private_v4,
public_v4 and public_v6.

View File

@ -4,7 +4,7 @@ munch
decorator
jsonpatch
ipaddress
os-client-config>=1.17.0,!=1.19.0
os-client-config>=1.20.0
requestsexceptions>=1.1.1
six

View File

@ -81,7 +81,7 @@ def get_server_private_ip(server, cloud=None):
# Short circuit the ports/networks search below with a heavily cached
# and possibly pre-configured network name
if cloud:
int_nets = cloud.get_internal_networks()
int_nets = cloud.get_internal_ipv4_networks()
for int_net in int_nets:
int_ip = get_server_ip(server, key_name=int_net['name'])
if int_ip is not None:
@ -123,7 +123,7 @@ def get_server_external_ipv4(cloud, server):
# Short circuit the ports/networks search below with a heavily cached
# and possibly pre-configured network name
ext_nets = cloud.get_external_networks()
ext_nets = cloud.get_external_ipv4_networks()
for ext_net in ext_nets:
ext_ip = get_server_ip(server, key_name=ext_net['name'])
if ext_ip is not None:

View File

@ -147,8 +147,19 @@ class OpenStackCloud(object):
self.secgroup_source = cloud_config.config['secgroup_source']
self.force_ipv4 = cloud_config.force_ipv4
# The first two aren't useful to us anymore, but we still do them
# because there are two methods that won't work without them
self._external_network_names = cloud_config.get_external_networks()
self._internal_network_names = cloud_config.get_internal_networks()
# Provide better error message for people with stale OCC
if cloud_config.get_external_ipv4_networks is None:
raise OpenStackCloudException(
"shade requires at least version 1.20.0 of os-client-config")
self._external_ipv4_names = cloud_config.get_external_ipv4_networks()
self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks()
self._external_ipv6_names = cloud_config.get_external_ipv6_networks()
self._internal_ipv6_names = cloud_config.get_internal_ipv6_networks()
self._nat_destination = cloud_config.get_nat_destination()
self._default_network = cloud_config.get_default_network()
@ -1596,6 +1607,10 @@ class OpenStackCloud(object):
with self._networks_lock:
self._external_networks = []
self._internal_networks = []
self._external_ipv4_networks = []
self._internal_ipv4_networks = []
self._external_ipv6_networks = []
self._internal_ipv6_networks = []
self._nat_destination_network = None
self._default_network_network = None
self._network_list_stamp = False
@ -1603,6 +1618,10 @@ class OpenStackCloud(object):
def _set_interesting_networks(self):
external_networks = []
internal_networks = []
external_ipv4_networks = []
internal_ipv4_networks = []
external_ipv6_networks = []
internal_ipv6_networks = []
nat_destination = None
default_network = None
@ -1623,7 +1642,7 @@ class OpenStackCloud(object):
return
for network in all_networks:
# External networks
# Old External networks
if (network['name'] in self._external_network_names
or network['id'] in self._external_network_names):
external_networks.append(network)
@ -1634,7 +1653,7 @@ class OpenStackCloud(object):
network['id'] not in self._internal_network_names):
external_networks.append(network)
# Internal networks
# Old Internal networks
if (network['name'] in self._internal_network_names
or network['id'] in self._internal_network_names):
internal_networks.append(network)
@ -1644,6 +1663,45 @@ class OpenStackCloud(object):
network['id'] not in self._external_network_names):
internal_networks.append(network)
# External IPv4 networks
if (network['name'] in self._external_ipv4_names
or network['id'] in self._external_ipv4_names):
external_ipv4_networks.append(network)
elif ((('router:external' in network
and network['router:external']) or
network.get('provider:physical_network')) and
network['name'] not in self._internal_ipv4_names and
network['id'] not in self._internal_ipv4_names):
external_ipv4_networks.append(network)
# Internal networks
if (network['name'] in self._internal_ipv4_names
or network['id'] in self._internal_ipv4_names):
internal_ipv4_networks.append(network)
elif (not network.get('router:external', False) and
not network.get('provider:physical_network') and
network['name'] not in self._external_ipv4_names and
network['id'] not in self._external_ipv4_names):
internal_ipv4_networks.append(network)
# External networks
if (network['name'] in self._external_ipv6_names
or network['id'] in self._external_ipv6_names):
external_ipv6_networks.append(network)
elif (network.get('router:external') and
network['name'] not in self._internal_ipv6_names and
network['id'] not in self._internal_ipv6_names):
external_ipv6_networks.append(network)
# Internal networks
if (network['name'] in self._internal_ipv6_names
or network['id'] in self._internal_ipv6_names):
internal_ipv6_networks.append(network)
elif (not network.get('router:external', False) and
network['name'] not in self._external_ipv6_names and
network['id'] not in self._external_ipv6_names):
internal_ipv6_networks.append(network)
# NAT Destination
if self._nat_destination in (
network['name'], network['id']):
@ -1707,6 +1765,34 @@ class OpenStackCloud(object):
" access and those networks could not be found".format(
network=net_name))
for net_name in self._external_ipv4_names:
if net_name not in [net['name'] for net in external_ipv4_networks]:
raise OpenStackCloudException(
"Networks: {network} was provided for external IPv4"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._internal_ipv4_names:
if net_name not in [net['name'] for net in internal_ipv4_networks]:
raise OpenStackCloudException(
"Networks: {network} was provided for internal IPv4"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._external_ipv6_names:
if net_name not in [net['name'] for net in external_ipv6_networks]:
raise OpenStackCloudException(
"Networks: {network} was provided for external IPv6"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._internal_ipv6_names:
if net_name not in [net['name'] for net in internal_ipv6_networks]:
raise OpenStackCloudException(
"Networks: {network} was provided for internal IPv6"
" access and those networks could not be found".format(
network=net_name))
if self._nat_destination and not nat_destination:
raise OpenStackCloudException(
'Network {network} was configured to be the'
@ -1723,6 +1809,10 @@ class OpenStackCloud(object):
self._external_networks = external_networks
self._internal_networks = internal_networks
self._external_ipv4_networks = external_ipv4_networks
self._internal_ipv4_networks = internal_ipv4_networks
self._external_ipv6_networks = external_ipv6_networks
self._internal_ipv6_networks = internal_ipv6_networks
self._nat_destination_network = nat_destination
self._default_network_network = default_network
@ -1761,6 +1851,9 @@ class OpenStackCloud(object):
def get_external_networks(self):
"""Return the networks that are configured to route northbound.
This should be avoided in favor of the specific ipv4/ipv6 method,
but is here for backwards compatibility.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
@ -1769,11 +1862,46 @@ class OpenStackCloud(object):
def get_internal_networks(self):
"""Return the networks that are configured to not route northbound.
This should be avoided in favor of the specific ipv4/ipv6 method,
but is here for backwards compatibility.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._internal_networks
def get_external_ipv4_networks(self):
"""Return the networks that are configured to route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._external_ipv4_networks
def get_internal_ipv4_networks(self):
"""Return the networks that are configured to not route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._internal_ipv4_networks
def get_external_ipv6_networks(self):
"""Return the networks that are configured to route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._external_ipv6_networks
def get_internal_ipv6_networks(self):
"""Return the networks that are configured to not route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._internal_ipv6_networks
def _has_floating_ips(self):
if not self._floating_ip_source:
return False
@ -3393,7 +3521,7 @@ class OpenStackCloud(object):
# Use given list to get first matching external network
floating_network_id = None
for net in network:
for ext_net in self.get_external_networks():
for ext_net in self.get_external_ipv4_networks():
if net in (ext_net['name'], ext_net['id']):
floating_network_id = ext_net['id']
break
@ -3406,8 +3534,8 @@ class OpenStackCloud(object):
net=network)
)
else:
# Get first existing external network
networks = self.get_external_networks()
# Get first existing external IPv4 network
networks = self.get_external_ipv4_networks()
if not networks:
raise OpenStackCloudResourceNotFound(
"unable to find an external network")
@ -3549,7 +3677,7 @@ class OpenStackCloud(object):
"unable to find network for floating ips with id "
"{0}".format(network_name_or_id))
else:
networks = self.get_external_networks()
networks = self.get_external_ipv4_networks()
if not networks:
raise OpenStackCloudResourceNotFound(
"Unable to find an external network in this cloud"
@ -4185,8 +4313,7 @@ class OpenStackCloud(object):
return True
# No external IPv4 network - no FIPs
# TODO(mordred) THIS IS get_external_ipv4_networks IN THE NEXT PATCH
networks = self.get_external_networks()
networks = self.get_external_ipv4_networks()
if not networks:
return False

View File

@ -214,6 +214,9 @@ class TestFloatingIP(base.TestCase):
mock_nova_client.servers.get.return_value = server
# TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS
# SERIOUSLY THIS TIME. NEXT PATCH - WHICH SHOULD ADD MOCKS FOR
# list_ports AND list_networks AND list_subnets. BUT THAT WOULD
# BE NOT ACTUALLY RELATED TO THIS PATCH. SO DO IT NEXT PATCH
mock_needs_floating_ip.return_value = True
self.cloud.add_ips_to_server(server_dict)

View File

@ -303,7 +303,7 @@ class TestFloatingIP(base.TestCase):
@patch.object(_utils, '_filter_list')
@patch.object(OpenStackCloud, '_neutron_create_floating_ip')
@patch.object(OpenStackCloud, '_neutron_list_floating_ips')
@patch.object(OpenStackCloud, 'get_external_networks')
@patch.object(OpenStackCloud, 'get_external_ipv4_networks')
@patch.object(OpenStackCloud, 'keystone_session')
def test__neutron_available_floating_ips(
self,
@ -340,7 +340,7 @@ class TestFloatingIP(base.TestCase):
@patch.object(_utils, '_filter_list')
@patch.object(OpenStackCloud, '_neutron_create_floating_ip')
@patch.object(OpenStackCloud, '_neutron_list_floating_ips')
@patch.object(OpenStackCloud, 'get_external_networks')
@patch.object(OpenStackCloud, 'get_external_ipv4_networks')
@patch.object(OpenStackCloud, 'keystone_session')
def test__neutron_available_floating_ips_network(
self,
@ -375,7 +375,7 @@ class TestFloatingIP(base.TestCase):
server=None
)
@patch.object(OpenStackCloud, 'get_external_networks')
@patch.object(OpenStackCloud, 'get_external_ipv4_networks')
@patch.object(OpenStackCloud, 'keystone_session')
def test__neutron_available_floating_ips_invalid_network(
self,
@ -621,7 +621,7 @@ class TestFloatingIP(base.TestCase):
@patch.object(OpenStackCloud, '_submit_create_fip')
@patch.object(OpenStackCloud, '_nat_destination_port')
@patch.object(OpenStackCloud, 'get_external_networks')
@patch.object(OpenStackCloud, 'get_external_ipv4_networks')
def test_create_floating_ip_no_port(
self, mock_get_ext_nets, mock_nat_destination_port,
mock_submit_create_fip):

View File

@ -61,6 +61,18 @@ class FakeCloud(object):
def get_external_networks(self):
return []
def get_internal_ipv4_networks(self):
return []
def get_external_ipv4_networks(self):
return []
def get_internal_ipv6_networks(self):
return []
def get_external_ipv6_networks(self):
return []
def list_server_security_groups(self, server):
return []
@ -107,6 +119,116 @@ SUBNETS_WITH_NAT = [
},
]
OSIC_NETWORKS = [
{
u'admin_state_up': True,
u'id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae',
u'mtu': 0,
u'name': u'GATEWAY_NET',
u'router:external': True,
u'shared': True,
u'status': u'ACTIVE',
u'subnets': [u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455'],
u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32'
},
{
u'admin_state_up': True,
u'id': u'405abfcc-77dc-49b2-a271-139619ac9b26',
u'mtu': 0,
u'name': u'openstackjenkins-network1',
u'router:external': False,
u'shared': False,
u'status': u'ACTIVE',
u'subnets': [u'a47910bc-f649-45db-98ec-e2421c413f4e'],
u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4'
},
{
u'admin_state_up': True,
u'id': u'54753d2c-0a58-4928-9b32-084c59dd20a6',
u'mtu': 0,
u'name': u'GATEWAY_NET_V6',
u'router:external': True,
u'shared': True,
u'status': u'ACTIVE',
u'subnets': [u'9c21d704-a8b9-409a-b56d-501cb518d380',
u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9'],
u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32'
}
]
OSIC_SUBNETS = [
{
u'allocation_pools': [{
u'end': u'172.99.106.254',
u'start': u'172.99.106.5'}],
u'cidr': u'172.99.106.0/24',
u'dns_nameservers': [u'69.20.0.164', u'69.20.0.196'],
u'enable_dhcp': True,
u'gateway_ip': u'172.99.106.1',
u'host_routes': [],
u'id': u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455',
u'ip_version': 4,
u'ipv6_address_mode': None,
u'ipv6_ra_mode': None,
u'name': u'GATEWAY_NET',
u'network_id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae',
u'subnetpool_id': None,
u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32'
},
{
u'allocation_pools': [{
u'end': u'10.0.1.254', u'start': u'10.0.1.2'}],
u'cidr': u'10.0.1.0/24',
u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'],
u'enable_dhcp': True,
u'gateway_ip': u'10.0.1.1',
u'host_routes': [],
u'id': u'a47910bc-f649-45db-98ec-e2421c413f4e',
u'ip_version': 4,
u'ipv6_address_mode': None,
u'ipv6_ra_mode': None,
u'name': u'openstackjenkins-subnet1',
u'network_id': u'405abfcc-77dc-49b2-a271-139619ac9b26',
u'subnetpool_id': None,
u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4'
},
{
u'allocation_pools': [{
u'end': u'10.255.255.254', u'start': u'10.0.0.2'}],
u'cidr': u'10.0.0.0/8',
u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'],
u'enable_dhcp': True,
u'gateway_ip': u'10.0.0.1',
u'host_routes': [],
u'id': u'9c21d704-a8b9-409a-b56d-501cb518d380',
u'ip_version': 4,
u'ipv6_address_mode': None,
u'ipv6_ra_mode': None,
u'name': u'GATEWAY_SUBNET_V6V4',
u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6',
u'subnetpool_id': None,
u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32'
},
{
u'allocation_pools': [{
u'end': u'2001:4800:1ae1:18:ffff:ffff:ffff:ffff',
u'start': u'2001:4800:1ae1:18::2'}],
u'cidr': u'2001:4800:1ae1:18::/64',
u'dns_nameservers': [u'2001:4860:4860::8888'],
u'enable_dhcp': True,
u'gateway_ip': u'2001:4800:1ae1:18::1',
u'host_routes': [],
u'id': u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9',
u'ip_version': 6,
u'ipv6_address_mode': u'dhcpv6-stateless',
u'ipv6_ra_mode': None,
u'name': u'GATEWAY_SUBNET_V6V6',
u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6',
u'subnetpool_id': None,
u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32'
}
]
class TestMeta(base.TestCase):
def test_find_nova_addresses_key_name(self):
@ -394,6 +516,61 @@ class TestMeta(base.TestCase):
mock_list_networks.assert_not_called()
mock_list_floating_ips.assert_not_called()
@mock.patch.object(shade.OpenStackCloud, 'list_floating_ips')
@mock.patch.object(shade.OpenStackCloud, 'list_subnets')
@mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups')
@mock.patch.object(shade.OpenStackCloud, 'get_image_name')
@mock.patch.object(shade.OpenStackCloud, 'get_flavor_name')
@mock.patch.object(shade.OpenStackCloud, 'has_service')
@mock.patch.object(shade.OpenStackCloud, 'list_networks')
def test_get_server_cloud_osic_split(
self, mock_list_networks, mock_has_service,
mock_get_flavor_name, mock_get_image_name,
mock_list_server_security_groups,
mock_list_subnets,
mock_list_floating_ips):
self.cloud._floating_ip_source = None
self.cloud.force_ipv4 = False
self.cloud._local_ipv6 = True
self.cloud._external_ipv4_names = ['GATEWAY_NET']
self.cloud._external_ipv6_names = ['GATEWAY_NET_V6']
self.cloud._internal_ipv4_names = ['GATEWAY_NET_V6']
self.cloud._internal_ipv6_names = []
mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec'
mock_get_flavor_name.return_value = 'm1.tiny'
mock_has_service.return_value = True
mock_list_subnets.return_value = OSIC_SUBNETS
mock_list_networks.return_value = OSIC_NETWORKS
srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer(
id='test-id', name='test-name', status='ACTIVE',
flavor={u'id': u'1'},
image={
'name': u'cirros-0.3.4-x86_64-uec',
u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'},
addresses={
'private': [{
'addr': "10.223.160.141",
'version': 4
}],
'public': [{
'addr': "104.130.246.91",
'version': 4
}, {
'addr': "2001:4800:7819:103:be76:4eff:fe05:8525",
'version': 6
}]
}
)))
self.assertEqual("10.223.160.141", srv['private_v4'])
self.assertEqual("104.130.246.91", srv['public_v4'])
self.assertEqual(
"2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6'])
self.assertEqual(
"2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip'])
mock_list_floating_ips.assert_not_called()
@mock.patch.object(shade.OpenStackCloud, 'has_service')
@mock.patch.object(shade.OpenStackCloud, 'list_subnets')
@mock.patch.object(shade.OpenStackCloud, 'list_networks')