From 46a5df2625b36e3b42b33a4b9f439bab0a87b1c7 Mon Sep 17 00:00:00 2001 From: Harald Jensas Date: Thu, 23 Feb 2017 18:56:49 +0100 Subject: [PATCH] Tripleo routed networks ironic inspector, and Undercloud * Enable the neutron segments service_plugin for routed provider networks. * Update controlplane network code to create segments for each subnet. A number of options related to ctlplane network is deprecated. More details in release notes. Implements: blueprint tripleo-routed-networks-ironic-inspector Implements: blueprint tripleo-routed-networks-deployment Depends-On: I33804bfd105a13c25d6057e8414e09957939e8af Change-Id: I4b384bab2af9f6ba07a137a37f4098a00ce18bc0 --- .../puppet-stack-config.yaml.template | 1 + instack_undercloud/tests/test_undercloud.py | 112 ++++++++++++++++- instack_undercloud/undercloud.py | 117 +++++++++++++++--- ...d-subnets-undercloud-64bb87222db0555b.yaml | 93 ++++++++++++++ 4 files changed, 304 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/routed-subnets-undercloud-64bb87222db0555b.yaml diff --git a/elements/puppet-stack-config/puppet-stack-config.yaml.template b/elements/puppet-stack-config/puppet-stack-config.yaml.template index 5196f67e8..e9e65dbe1 100644 --- a/elements/puppet-stack-config/puppet-stack-config.yaml.template +++ b/elements/puppet-stack-config/puppet-stack-config.yaml.template @@ -275,6 +275,7 @@ neutron::keystone::auth::admin_url: {{UNDERCLOUD_ENDPOINT_NEUTRON_ADMIN}} neutron::keystone::auth::password: {{UNDERCLOUD_NEUTRON_PASSWORD}} neutron::keystone::auth::region: "%{hiera('keystone_region')}" neutron::plugins::ml2::extension_drivers: 'port_security' +neutron::service_plugins: ['segments'] # Ceilometer ceilometer::debug: "%{hiera('debug')}" diff --git a/instack_undercloud/tests/test_undercloud.py b/instack_undercloud/tests/test_undercloud.py index 5b4e3c5f0..b06888d5c 100644 --- a/instack_undercloud/tests/test_undercloud.py +++ b/instack_undercloud/tests/test_undercloud.py @@ -907,7 +907,7 @@ class TestConfigureSshKeys(base.BaseTestCase): self._test_configure_ssh_keys(mock_eui, False) -class TestPostConfig(base.BaseTestCase): +class TestPostConfig(BaseTestCase): @mock.patch('os_client_config.make_client') @mock.patch('instack_undercloud.undercloud._migrate_to_convergence') @mock.patch('instack_undercloud.undercloud._ensure_node_resource_classes') @@ -1430,6 +1430,8 @@ class TestPostConfig(base.BaseTestCase): def _neutron_mocks(self): mock_sdk = mock.MagicMock() mock_sdk.network.create_network = mock.Mock() + mock_sdk.network.create_segment = mock.Mock() + mock_sdk.network.update_segment = mock.Mock() mock_sdk.network.delete_segment = mock.Mock() mock_sdk.network.create_subnet = mock.Mock() mock_sdk.network.update_subnet = mock.Mock() @@ -1445,12 +1447,36 @@ class TestPostConfig(base.BaseTestCase): name='ctlplane', provider_network_type='flat', provider_physical_network='ctlplane') + def test_delete_default_segment(self): + mock_sdk = self._neutron_mocks() + mock_sdk.network.networks.return_value = iter([]) + segment_mock = mock.Mock() + mock_sdk.network.segments.return_value = iter([segment_mock]) + undercloud._ensure_neutron_network(mock_sdk) + mock_sdk.network.delete_segment.assert_called_with( + segment_mock.id) + def test_network_exists(self): mock_sdk = self._neutron_mocks() mock_sdk.network.networks.return_value = iter(['ctlplane']) undercloud._ensure_neutron_network(mock_sdk) mock_sdk.network.create_network.assert_not_called() + def test_segment_create(self): + mock_sdk = self._neutron_mocks() + undercloud._neutron_segment_create(mock_sdk, 'ctlplane-subnet', + 'network_id', 'ctlplane') + mock_sdk.network.create_segment.assert_called_with( + name='ctlplane-subnet', network_id='network_id', + physical_network='ctlplane', network_type='flat') + + def test_segment_update(self): + mock_sdk = self._neutron_mocks() + undercloud._neutron_segment_update(mock_sdk, + 'network_id', 'ctlplane-subnet') + mock_sdk.network.update_segment.assert_called_with( + 'network_id', name='ctlplane-subnet') + def test_subnet_create(self): mock_sdk = self._neutron_mocks() host_routes = [{'destination': '169.254.169.254/32', @@ -1459,12 +1485,12 @@ class TestPostConfig(base.BaseTestCase): undercloud._neutron_subnet_create(mock_sdk, 'network_id', '192.168.24.0/24', '192.168.24.1', host_routes, allocation_pool, - 'ctlplane-subnet') + 'ctlplane-subnet', 'segment_id') mock_sdk.network.create_subnet.assert_called_with( name='ctlplane-subnet', cidr='192.168.24.0/24', gateway_ip='192.168.24.1', host_routes=host_routes, enable_dhcp=True, ip_version='4', allocation_pools=allocation_pool, - network_id='network_id') + network_id='network_id', segment_id='segment_id') def test_subnet_update(self): mock_sdk = self._neutron_mocks() @@ -1478,6 +1504,86 @@ class TestPostConfig(base.BaseTestCase): 'subnet_id', name='ctlplane-subnet', gateway_ip='192.168.24.1', host_routes=host_routes, allocation_pools=allocation_pool) + @mock.patch('instack_undercloud.undercloud._neutron_subnet_update') + @mock.patch('instack_undercloud.undercloud._get_subnet') + def test_no_neutron_segments_if_pre_segments_undercloud( + self, mock_get_subnet, mock_neutron_subnet_update): + mock_sdk = self._neutron_mocks() + mock_subnet = mock.Mock() + mock_subnet.segment_id = None + mock_get_subnet.return_value = mock_subnet + undercloud._config_neutron_segments_and_subnets(mock_sdk, + 'ctlplane_id') + mock_sdk.network.create_segment.assert_not_called() + mock_sdk.network.update_segment.assert_not_called() + mock_neutron_subnet_update.called_once() + + @mock.patch('instack_undercloud.undercloud._neutron_segment_create') + @mock.patch('instack_undercloud.undercloud._neutron_subnet_create') + @mock.patch('instack_undercloud.undercloud._get_segment') + @mock.patch('instack_undercloud.undercloud._get_subnet') + def test_segment_and_subnet_create(self, mock_get_subnet, mock_get_segment, + mock_neutron_subnet_create, + mock_neutron_segment_create): + mock_sdk = self._neutron_mocks() + mock_get_subnet.return_value = None + mock_get_segment.return_value = None + undercloud._config_neutron_segments_and_subnets(mock_sdk, + 'ctlplane_id') + mock_neutron_segment_create.assert_called_with( + mock_sdk, 'ctlplane-subnet', 'ctlplane_id', 'ctlplane') + host_routes = [{'destination': '169.254.169.254/32', + 'nexthop': '192.168.24.1'}] + allocation_pool = [{'start': '192.168.24.5', 'end': '192.168.24.24'}] + mock_neutron_subnet_create.assert_called_with( + mock_sdk, 'ctlplane_id', '192.168.24.0/24', '192.168.24.1', + host_routes, allocation_pool, 'ctlplane-subnet', + mock_neutron_segment_create().id) + + @mock.patch('instack_undercloud.undercloud._neutron_segment_update') + @mock.patch('instack_undercloud.undercloud._neutron_subnet_update') + @mock.patch('instack_undercloud.undercloud._get_segment') + @mock.patch('instack_undercloud.undercloud._get_subnet') + def test_segment_and_subnet_update(self, mock_get_subnet, mock_get_segment, + mock_neutron_subnet_update, + mock_neutron_segment_update): + mock_sdk = self._neutron_mocks() + mock_subnet = mock.Mock() + mock_subnet.id = 'subnet_id' + mock_subnet.segment_id = 'segment_id' + mock_get_subnet.return_value = mock_subnet + mock_segment = mock.Mock() + mock_get_segment.return_value = mock_segment + mock_segment.id = 'segment_id' + undercloud._config_neutron_segments_and_subnets(mock_sdk, + 'ctlplane_id') + mock_neutron_segment_update.assert_called_with( + mock_sdk, mock_subnet.segment_id, 'ctlplane-subnet') + host_routes = [{'destination': '169.254.169.254/32', + 'nexthop': '192.168.24.1'}] + allocation_pool = [{'start': '192.168.24.5', 'end': '192.168.24.24'}] + mock_neutron_subnet_update.assert_called_with( + mock_sdk, 'subnet_id', '192.168.24.1', host_routes, + allocation_pool, 'ctlplane-subnet') + + @mock.patch('instack_undercloud.undercloud._get_segment') + @mock.patch('instack_undercloud.undercloud._get_subnet') + def test_local_subnet_cidr_conflict(self, mock_get_subnet, + mock_get_segment): + mock_sdk = self._neutron_mocks() + mock_sdk = self._neutron_mocks() + mock_subnet = mock.Mock() + mock_subnet.id = 'subnet_id' + mock_subnet.segment_id = 'existing_segment_id' + mock_get_subnet.return_value = mock_subnet + mock_segment = mock.Mock() + mock_get_segment.return_value = mock_segment + mock_segment.id = 'segment_id' + self.assertRaises( + RuntimeError, + undercloud._config_neutron_segments_and_subnets, [mock_sdk], + ['ctlplane_id']) + class TestUpgradeFact(base.BaseTestCase): @mock.patch('instack_undercloud.undercloud._run_command') diff --git a/instack_undercloud/undercloud.py b/instack_undercloud/undercloud.py index fcd95eb8f..43660e6cd 100644 --- a/instack_undercloud/undercloud.py +++ b/instack_undercloud/undercloud.py @@ -2033,6 +2033,11 @@ def _ensure_neutron_network(sdk): name=PHYSICAL_NETWORK, provider_network_type='flat', provider_physical_network=PHYSICAL_NETWORK) LOG.info("Network created %s", network) + # (hjensas) Delete the default segment, we create a new segment + # per subnet later. + segments = list(sdk.network.segments(network=network.id)) + sdk.network.delete_segment(segments[0].id) + LOG.info("Default segment on network %s deleted.", network.name) else: LOG.info("Not creating %s network, because it already exists.", PHYSICAL_NETWORK) @@ -2045,7 +2050,7 @@ def _ensure_neutron_network(sdk): def _neutron_subnet_create(sdk, network_id, cidr, gateway, host_routes, - allocation_pool, name): + allocation_pool, name, segment_id): try: # DHCP_START contains a ":" then assume a IPv6 subnet if ':' in allocation_pool[0]['start']: @@ -2060,7 +2065,8 @@ def _neutron_subnet_create(sdk, network_id, cidr, gateway, host_routes, ipv6_address_mode='dhcpv6-stateless', ipv6_ra_mode='dhcpv6-stateless', allocation_pools=allocation_pool, - network_id=network_id) + network_id=network_id, + segment_id=segment_id) else: subnet = sdk.network.create_subnet( name=name, @@ -2070,7 +2076,8 @@ def _neutron_subnet_create(sdk, network_id, cidr, gateway, host_routes, enable_dhcp=True, ip_version='4', allocation_pools=allocation_pool, - network_id=network_id) + network_id=network_id, + segment_id=segment_id) LOG.info("Subnet created %s", subnet) except Exception as e: LOG.error("Create subnet %s failed: %s", name, e) @@ -2097,6 +2104,30 @@ def _neutron_subnet_update(sdk, subnet_id, gateway, host_routes, raise +def _neutron_segment_create(sdk, name, network_id, phynet): + try: + segment = sdk.network.create_segment( + name=name, + network_id=network_id, + physical_network=phynet, + network_type='flat') + LOG.info("Neutron Segment created %s", segment) + except Exception as e: + LOG.info("Neutron Segment %s create failed %s", name, e) + raise + + return segment + + +def _neutron_segment_update(sdk, segment_id, name): + try: + segment = sdk.network.update_segment(segment_id, name=name) + LOG.info("Neutron Segment updated %s", segment) + except Exception as e: + LOG.info("Neutron Segment %s update failed %s", name, e) + raise + + def _ensure_neutron_router(sdk, name, subnet_id): try: router = sdk.network.create_router(name=name, admin_state_up='true') @@ -2115,25 +2146,79 @@ def _get_subnet(sdk, cidr, network_id): return False if not subnet else subnet[0] +def _get_segment(sdk, phy, network_id): + try: + segment = list(sdk.network.segments(physical_network=phy, + network_id=network_id)) + except Exception: + raise + + return False if not segment else segment[0] + + def _config_neutron_segments_and_subnets(sdk, ctlplane_id): s = CONF.get(CONF.local_subnet) - host_routes = [{'destination': '169.254.169.254/32', - 'nexthop': str(netaddr.IPNetwork(CONF.local_ip).ip)}] - allocation_pool = [{'start': s.dhcp_start, 'end': s.dhcp_end}] - - subnet = _get_subnet(sdk, CONF.network_cidr, ctlplane_id) - if subnet: + subnet = _get_subnet(sdk, s.cidr, ctlplane_id) + if subnet and not subnet.segment_id: + LOG.warn("Local subnet %s already exists and is not associated with a " + "network segment. Any additional subnets will be ignored.", + CONF.local_subnet) + host_routes = [{'destination': '169.254.169.254/32', + 'nexthop': str(netaddr.IPNetwork(CONF.local_ip).ip)}] + allocation_pool = [{'start': s.dhcp_start, 'end': s.dhcp_end}] _neutron_subnet_update(sdk, subnet.id, s.gateway, host_routes, allocation_pool, CONF.local_subnet) + # If the subnet is IPv6 we need to start a router so that router + # advertisments are sent out for stateless IP addressing to work. + if ':' in s.dhcp_start: + _ensure_neutron_router(sdk, CONF.local_subnet, subnet.id) else: - subnet = _neutron_subnet_create(sdk, ctlplane_id, s.cidr, s.gateway, - host_routes, allocation_pool, - CONF.local_subnet) + for name in CONF.subnets: + s = CONF.get(name) - # If the subnet is IPv6 we need to start a router so that router - # advertisments are sent out for stateless IP addressing to work. - if ':' in CONF.dhcp_start: - _ensure_neutron_router(sdk, CONF.local_subnet, subnet.id) + phynet = name + if name == CONF.local_subnet: + phynet = PHYSICAL_NETWORK + + metadata_nexthop = s.gateway + if str(netaddr.IPNetwork(CONF.local_ip).ip) in s.cidr: + metadata_nexthop = str(netaddr.IPNetwork(CONF.local_ip).ip) + + host_routes = [{'destination': '169.254.169.254/32', + 'nexthop': metadata_nexthop}] + allocation_pool = [{'start': s.dhcp_start, 'end': s.dhcp_end}] + + subnet = _get_subnet(sdk, s.cidr, ctlplane_id) + segment = _get_segment(sdk, phynet, ctlplane_id) + + if name == CONF.local_subnet: + if ((subnet and not segment) or + (subnet and segment and subnet.segment_id != segment.id)): + LOG.error( + 'The cidr: %s of the local subnet is already used in ' + 'subnet: %s associated with segment_id: %s.' % + (s.cidr, subnet.id, subnet.segment_id)) + raise RuntimeError('Local subnet cidr already associated.') + + if subnet: + _neutron_segment_update(sdk, subnet.segment_id, name) + _neutron_subnet_update(sdk, subnet.id, s.gateway, host_routes, + allocation_pool, name) + else: + if segment: + _neutron_segment_update(sdk, segment.id, name) + else: + segment = _neutron_segment_create(sdk, name, + ctlplane_id, phynet) + subnet = _neutron_subnet_create(sdk, ctlplane_id, s.cidr, + s.gateway, host_routes, + allocation_pool, name, + segment.id) + + # If the subnet is IPv6 we need to start a router so that router + # advertisments are sent out for stateless IP addressing to work. + if ':' in s.dhcp_start: + _ensure_neutron_router(sdk, name, subnet.id) def _handle_upgrade_fact(upgrade=False): diff --git a/releasenotes/notes/routed-subnets-undercloud-64bb87222db0555b.yaml b/releasenotes/notes/routed-subnets-undercloud-64bb87222db0555b.yaml new file mode 100644 index 000000000..107382135 --- /dev/null +++ b/releasenotes/notes/routed-subnets-undercloud-64bb87222db0555b.yaml @@ -0,0 +1,93 @@ +--- +prelude: > + With support for routed networks, several options are deprecated and the + way undercloud networking is defined in the configuration file has several + changes. Please refer to the **Deprecation Notes** and **Upgrade notes** + section for details. +features: + - Routed networks support adds the ability to configure Ironic Inspector + and Neutron provisioning network in the undercloud to enable provisioning + of nodes via DHCP-relay to the undercloud from remote routed network + segments. +upgrade: + - | + With support for routed network segments, several options are deprecated + and the way undercloud networking is defined in the configuration file has + several changes. + + **New option:** ``subnets`` A list of subnets. One entry for each routed + network segment used for provisioning and introspection. For each network + segment a section/group needs to be added to the configuration file + specifying the following subnet options: + + ====================== ================================================ + option Description + ====================== ================================================ + ``cidr`` Network CIDR for the subnet. + ``dhcp_start`` Start of DHCP allocation range for PXE and DHCP. + ``dhcp_end`` End of DHCP allocation range for PXE and DHCP. + ``inspection_iprange`` Temporary IP range that will be given to nodes + during the inspection process. + ``gateway`` Network(subnet) gateway/router. + ``masquerade`` (Boolean) If ``True`` the undercloud will + masquerade this network for external access. + ====================== ================================================ + + **New option:** ``local_subnet`` The name of the local subnet, where the + PXE boot and DHCP interfaces for overcloud instances is located. The IP + address of the local_ip/local_interface should reside in this subnet. + + .. Note:: Upgrade with migration to routed networks support is not + possible. + + Routed networks use the neutron segments service_plugin, this + plugin adds functionality that allows subnet to be associated + with a network segment. It is currently not possible to add + segment association to an existing subnet, because of this we + cannot add segment association to the existing ctlplane subnet + on the upgraded undercloud. The existing ctlplane network and + subnet will still be in place after an upgrade and the upgraded + undercloud can continue to manage the existing overcloud. + + The following example shows what changes to make to the configuration to + move to the new model. + + Replace usage of deprecated options:: + + [DEFAULT] + network_gateway = 192.168.24.1 + network_cidr = 192.168.24.0/24 + dhcp_start = 192.168.24.5 + dhcp_end = 192.168.24.24 + inspection_iprange = 192.168.24.100,192.168.24.120 + masquerade_network = 192.168.24.0/24 + + replace with:: + + [DEFAULT] + subnets = subnet0 + local_subnet = subnet0 + + [subnet0] + cidr = 192.168.24.0/24 + dhcp_start = 192.168.24.5 + dhcp_end = 192.168.24.24 + inspection_iprange = 192.168.24.100,192.168.24.120 + gateway = 192.168.24.1 + masquerade = True +deprecations: + - With support for routed networks/subnets the ``network_gateway`` option in + the ``[DEFAULT]`` section is deprecated. Moved to per-subnet options + group. + - With support for routed networks/subnets the ``network_cidr`` option in + the ``[DEFAULT]`` section is deprecated. Moved to per-subnet options + group. + - With support for routed networks/subnets the ``dhcp_start`` and + ``dhcp_end`` options in the ``[DEFAULT]`` section are deprecated. Moved to + per-subnet options group. + - With support for routed networks/subnets the ``inspection_iprange`` option + in the ``[DEFAULT]`` section is deprecated. Moved to per-subnet options + group. + - With support for routed networks/subnets the ``masquerade_network`` + option in the ``[DEFAULT]`` section is deprecated. Use the boolean option + in each subnet group.