From a7d289109d86277006d46194db6413fdadad4074 Mon Sep 17 00:00:00 2001 From: Harald Jensas Date: Sat, 13 Jan 2018 13:00:39 +0100 Subject: [PATCH] Move ctlplane network/subnet setup to python For routed networks deployment we need to create multiple subnets on the controlplane network. The logic to implement this is more involved and so doing this in python makes sense. This change moves the current network config from 98-undercloud-setup to a python implementation. Change-Id: I0168991a0fbd9dacf708ba2bdbc13248414affbe Implements: blueprint tripleo-routed-networks-deployment --- .../post-configure.d/98-undercloud-setup | 75 ----------- instack_undercloud/tests/test_undercloud.py | 56 ++++++++ instack_undercloud/undercloud.py | 126 +++++++++++++++++- 3 files changed, 181 insertions(+), 76 deletions(-) diff --git a/elements/undercloud-install/os-refresh-config/post-configure.d/98-undercloud-setup b/elements/undercloud-install/os-refresh-config/post-configure.d/98-undercloud-setup index 6853beb40..31816c493 100755 --- a/elements/undercloud-install/os-refresh-config/post-configure.d/98-undercloud-setup +++ b/elements/undercloud-install/os-refresh-config/post-configure.d/98-undercloud-setup @@ -37,81 +37,6 @@ if [ -e /usr/sbin/getenforce ]; then fi fi -UNDERCLOUD_IP=$(os-apply-config --key local-ip --type netaddress) -export UNDERCLOUD_IP - -DHCP_START=$(os-apply-config --key neutron.dhcp_start --type netaddress) -DHCP_END=$(os-apply-config --key neutron.dhcp_end --type netaddress) -NETWORK_CIDR=$(os-apply-config --key neutron.network_cidr --type raw) -NETWORK_GATEWAY=$(os-apply-config --key neutron.network_gateway --type netaddress) -METADATA_SERVER=$UNDERCLOUD_IP -PHYSICAL_NETWORK=ctlplane - -# DHCP_START contains a ":" then assume a IPv6 subnet -SUBNET_VERSION_STRING="--ip-version 4" -SUBNET_ROUTE_STRING="--host-route destination=169.254.169.254/32,nexthop=$METADATA_SERVER" -if [[ $DHCP_START =~ : ]] ; then - SUBNET_VERSION_STRING="--ip-version 6 --ipv6-address-mode dhcpv6-stateless --ipv6-ra-mode dhcpv6-stateless" - SUBNET_ROUTE_STRING="" -fi - -net_create=1 -ctlplane_id=$(neutron net-list -f csv -c id -c name --quote none | tail -n +2 | grep ctlplane | cut -d, -f 1) -subnet_ids=$(neutron subnet-list -f csv -c id --quote none | tail -n +2) -subnet_id= - -for subnet_id in $subnet_ids; do - network_id=$(neutron subnet-show -f value -c network_id $subnet_id) - if [ "$network_id" = "$ctlplane_id" ]; then - break - fi -done - -if [ -n "$subnet_id" ]; then - cidr=$(neutron subnet-show $subnet_id -f value -c cidr) - # If the cidr's are equal, we can get by with just a network update - if [ "$cidr" = "$NETWORK_CIDR" ]; then - net_create=0 - neutron subnet-update $subnet_id \ - --allocation-pool start=$DHCP_START,end=$DHCP_END \ - --gateway $NETWORK_GATEWAY $SUBNET_ROUTE_STRING - else - echo "New cidr $NETWORK_CIDR does not equal old cidr $cidr" - echo "Will attempt to delete and recreate subnet $subnet_id" - fi -fi - -if [ "$net_create" -eq "1" ]; then - # Delete the route, subnet and network to make sure it doesn't already exist - if neutron router-show ctlplane-router ; then - neutron router-interface-delete ctlplane-router ctlplane-subnet - neutron router-delete ctlplane-router - fi - if neutron subnet-list | grep start; then - neutron subnet-delete $(neutron subnet-list | grep start | awk '{print $2}') - fi - if neutron net-show ctlplane; then - neutron net-delete ctlplane - fi - - neutron net-create ctlplane \ - --provider:network_type flat \ - --provider:physical_network ctlplane - - neutron subnet-create --name ctlplane-subnet \ - --allocation-pool start=$DHCP_START,end=$DHCP_END \ - --gateway $NETWORK_GATEWAY \ - $SUBNET_VERSION_STRING $SUBNET_ROUTE_STRING \ - ctlplane $NETWORK_CIDR - - # If ctlplane-subnet is IPv6 we need to start a router so the router advertisments are sent out - # for statless IP addressing to work. - if [[ $DHCP_START =~ : ]] ; then - neutron router-create ctlplane-router - neutron router-interface-add ctlplane-router ctlplane-subnet - fi -fi - # Disable nova quotas openstack quota set --cores -1 --instances -1 --ram -1 $(openstack project show admin | awk '$2=="id" {print $4}') diff --git a/instack_undercloud/tests/test_undercloud.py b/instack_undercloud/tests/test_undercloud.py index 6b4d38507..1e6f4b24a 100644 --- a/instack_undercloud/tests/test_undercloud.py +++ b/instack_undercloud/tests/test_undercloud.py @@ -773,6 +773,9 @@ class TestPostConfig(base.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') + @mock.patch( + 'instack_undercloud.undercloud._config_neutron_segments_and_subnets') + @mock.patch('instack_undercloud.undercloud._ensure_neutron_network') @mock.patch('instack_undercloud.undercloud._member_role_exists') @mock.patch('instack_undercloud.undercloud._get_session') @mock.patch('ironicclient.client.get_client', autospec=True) @@ -790,6 +793,8 @@ class TestPostConfig(base.BaseTestCase): mock_copy_stackrc, mock_delete, mock_mistral_client, mock_swift_client, mock_nova_client, mock_ir_client, mock_get_session, mock_member_role_exists, + mock_ensure_neutron_network, + mock_config_neutron_segments_and_subnets, mock_resource_classes, mock_migrate_to_convergence, mock_make_client): instack_env = { @@ -1284,6 +1289,57 @@ class TestPostConfig(base.BaseTestCase): mock_cmce.assert_called_once_with(instack_env, mock_mistral) mock_create.assert_called_once_with(mock_mistral, ['hut8']) + def _neutron_mocks(self): + mock_sdk = mock.MagicMock() + mock_sdk.network.create_network = mock.Mock() + mock_sdk.network.delete_segment = mock.Mock() + mock_sdk.network.create_subnet = mock.Mock() + mock_sdk.network.update_subnet = mock.Mock() + return mock_sdk + + def test_network_create(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.create_network.assert_called_with( + name='ctlplane', provider_network_type='flat', + provider_physical_network='ctlplane') + + 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_subnet_create(self): + mock_sdk = self._neutron_mocks() + 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'}] + undercloud._neutron_subnet_create(mock_sdk, 'network_id', + '192.168.24.0/24', '192.168.24.1', + host_routes, allocation_pool, + 'ctlplane-subnet') + 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') + + def test_subnet_update(self): + mock_sdk = self._neutron_mocks() + 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'}] + undercloud._neutron_subnet_update(mock_sdk, 'subnet_id', + '192.168.24.1', host_routes, + allocation_pool, 'ctlplane-subnet') + mock_sdk.network.update_subnet.assert_called_with( + 'subnet_id', name='ctlplane-subnet', gateway_ip='192.168.24.1', + host_routes=host_routes, allocation_pools=allocation_pool) + class TestUpgradeFact(base.BaseTestCase): @mock.patch('instack_undercloud.undercloud._run_command') diff --git a/instack_undercloud/undercloud.py b/instack_undercloud/undercloud.py index 6367df0b0..fab244d21 100644 --- a/instack_undercloud/undercloud.py +++ b/instack_undercloud/undercloud.py @@ -21,6 +21,7 @@ import glob import hashlib import json import logging +import netaddr import os import platform import re @@ -111,7 +112,9 @@ log can be found at %(log_file)s. # We need 8 GB, leave a little room for variation in what 8 GB means on # different platforms. REQUIRED_MB = 7680 - +# Control plane network name +PHYSICAL_NETWORK = 'ctlplane' +CTLPLANE_SUBNET_NAME = 'ctlplane-subnet' # When adding new options to the lists below, make sure to regenerate the # sample config by running "tox -e genconfig" in the project root. @@ -1808,6 +1811,16 @@ def _post_config(instack_env, upgrade): ironic = ir_client.get_client(1, session=sess, os_ironic_api_version='1.21') + sdk = os_client_config.make_sdk(auth_url=auth_url, + project_name=project, + username=user, + password=password, + project_domain_name='Default', + user_domain_name='Default') + + network = _ensure_neutron_network(sdk) + _config_neutron_segments_and_subnets(sdk, network.id) + _configure_ssh_keys(nova) _ensure_ssh_selinux_permission() _delete_default_flavors(nova) @@ -1848,6 +1861,117 @@ def _post_config(instack_env, upgrade): _migrate_to_convergence(heat) +def _ensure_neutron_network(sdk): + try: + network = list(sdk.network.networks(name=PHYSICAL_NETWORK)) + if not network: + network = sdk.network.create_network( + name=PHYSICAL_NETWORK, provider_network_type='flat', + provider_physical_network=PHYSICAL_NETWORK) + LOG.info("Network created %s", network) + else: + LOG.info("Not creating %s network, because it already exists.", + PHYSICAL_NETWORK) + network = network[0] + except Exception as e: + LOG.info("Network create/update failed %s", e) + raise + + return network + + +def _neutron_subnet_create(sdk, network_id, cidr, gateway, host_routes, + allocation_pool, name): + try: + # DHCP_START contains a ":" then assume a IPv6 subnet + if ':' in allocation_pool[0]['start']: + host_routes = '' + subnet = sdk.network.create_subnet( + name=name, + cidr=cidr, + gateway_ip=gateway, + host_routes=host_routes, + enable_dhcp=True, + ip_version='6', + ipv6_address_mode='dhcpv6-stateless', + ipv6_ra_mode='dhcpv6-stateless', + allocation_pools=allocation_pool, + network_id=network_id) + else: + subnet = sdk.network.create_subnet( + name=name, + cidr=cidr, + gateway_ip=gateway, + host_routes=host_routes, + enable_dhcp=True, + ip_version='4', + allocation_pools=allocation_pool, + network_id=network_id) + LOG.info("Subnet created %s", subnet) + except Exception as e: + LOG.error("Create subnet %s failed: %s", name, e) + raise + + return subnet + + +def _neutron_subnet_update(sdk, subnet_id, gateway, host_routes, + allocation_pool, name): + try: + # DHCP_START contains a ":" then assume a IPv6 subnet + if ':' in allocation_pool[0]['start']: + host_routes = '' + subnet = sdk.network.update_subnet( + subnet_id, + name=name, + gateway_ip=gateway, + host_routes=host_routes, + allocation_pools=allocation_pool) + LOG.info("Subnet updated %s", subnet) + except Exception as e: + LOG.error("Update subnet %s 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') + sdk.network.add_interface_to_router(router.id, subnet_id=subnet_id) + except Exception as e: + LOG.error("Create router for subnet %s failed: %s", name, e) + raise + + +def _get_subnet(sdk, cidr, network_id): + try: + subnet = list(sdk.network.subnets(cidr=cidr, network_id=network_id)) + except Exception: + raise + + return False if not subnet else subnet[0] + + +def _config_neutron_segments_and_subnets(sdk, ctlplane_id): + host_routes = [{'destination': '169.254.169.254/32', + 'nexthop': str(netaddr.IPNetwork(CONF.local_ip).ip)}] + allocation_pool = [{'start': CONF.dhcp_start, 'end': CONF.dhcp_end}] + + subnet = _get_subnet(sdk, CONF.network_cidr, ctlplane_id) + if subnet: + _neutron_subnet_update(sdk, subnet.id, CONF.network_gateway, + host_routes, allocation_pool, + CTLPLANE_SUBNET_NAME) + else: + subnet = _neutron_subnet_create(sdk, ctlplane_id, CONF.network_cidr, + CONF.network_gateway, host_routes, + allocation_pool, CTLPLANE_SUBNET_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, CTLPLANE_SUBNET_NAME, subnet.id) + + def _handle_upgrade_fact(upgrade=False): """Create an upgrade fact for use in puppet