From 6832f734d00f1c118068db1fbd65370ad7f5d178 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Aug 2016 17:21:56 -0500 Subject: [PATCH] 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 --- .../dual-stack-networks-8a81941c97d28deb.yaml | 8 + requirements.txt | 2 +- shade/meta.py | 4 +- shade/openstackcloud.py | 143 +++++++++++++- shade/tests/unit/test_floating_ip_common.py | 3 + shade/tests/unit/test_floating_ip_neutron.py | 8 +- shade/tests/unit/test_meta.py | 177 ++++++++++++++++++ 7 files changed, 330 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml diff --git a/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml b/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml new file mode 100644 index 000000000..70e28e7b1 --- /dev/null +++ b/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index c4465074a..54e1d450c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/shade/meta.py b/shade/meta.py index 8c489ce19..492367d98 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -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: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0839b764e..442e65d3b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -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 diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index ff561c645..31515f239 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -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) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 84fbedf25..61713308d 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -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): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 2a92a35c8..48d45f6b8 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -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')