diff --git a/environments/undercloud.yaml b/environments/undercloud.yaml
index 4ebb66ac43..ca997aaae4 100644
--- a/environments/undercloud.yaml
+++ b/environments/undercloud.yaml
@@ -90,3 +90,10 @@ parameter_defaults:
   #     - ip_netmask: 192.168.26.0/24
   #       next_hop: 192.168.24.1
   ControlPlaneStaticRoutes: []
+  UndercloudCtlplaneSubnets:
+    ctlplane-subnet:
+      NetworkCidr: '192.168.24.0/24'
+      NetworkGateway: '192.168.24.1'
+      DhcpRangeStart: '192.168.24.5'
+      DhcpRangeEnd: '192.168.24.24'
+  UndercloudCtlplaneLocalSubnet: 'ctlplane-subnet'
diff --git a/extraconfig/post_deploy/undercloud_ctlplane_network.py b/extraconfig/post_deploy/undercloud_ctlplane_network.py
new file mode 100644
index 0000000000..8f8671aed2
--- /dev/null
+++ b/extraconfig/post_deploy/undercloud_ctlplane_network.py
@@ -0,0 +1,273 @@
+#!/usr/bin/python
+
+import json
+import netaddr
+import os
+import os_client_config
+import subprocess
+
+CTLPLANE_NETWORK_NAME = 'ctlplane'
+
+AUTH_URL = os.environ['auth_url']
+ADMIN_PASSWORD = os.environ['admin_password']
+CONF = json.loads(os.environ['config'])
+
+
+def _run_command(args, env=None, name=None):
+    """Run the command defined by args and return its output
+
+    :param args: List of arguments for the command to be run.
+    :param env: Dict defining the environment variables. Pass None to use
+        the current environment.
+    :param name: User-friendly name for the command being run. A value of
+        None will cause args[0] to be used.
+    """
+    if name is None:
+        name = args[0]
+
+    if env is None:
+        env = os.environ
+    env = env.copy()
+
+    # When running a localized python script, we need to tell it that we're
+    # using utf-8 for stdout, otherwise it can't tell because of the pipe.
+    env['PYTHONIOENCODING'] = 'utf8'
+
+    try:
+        return subprocess.check_output(args,
+                                       stderr=subprocess.STDOUT,
+                                       env=env).decode('utf-8')
+    except subprocess.CalledProcessError as ex:
+        print('ERROR: %s failed: %s' % (name, ex.output))
+        raise
+
+
+def _ensure_neutron_network(sdk):
+    try:
+        network = list(sdk.network.networks(name=CTLPLANE_NETWORK_NAME))
+        if not network:
+            network = sdk.network.create_network(
+                name=CTLPLANE_NETWORK_NAME,
+                provider_network_type='flat',
+                provider_physical_network=CONF['physical_network'],
+                mtu=CONF['mtu'])
+            print('INFO: Network created %s' % network)
+            # (hjensas) Delete the default segment, we create a new segment
+            # per subnet later.
+            segments = list(sdk.network.segments(network_id=network.id))
+            sdk.network.delete_segment(segments[0].id)
+            print('INFO: Default segment on network %s deleted.' %
+                  network.name)
+        else:
+            network = sdk.network.update_network(
+                network[0].id,
+                name=CTLPLANE_NETWORK_NAME,
+                mtu=CONF['mtu'])
+            print('INFO: Network updated %s' % network)
+    except Exception:
+        print('ERROR: Network create/update failed.')
+        raise
+
+    return network
+
+
+def _neutron_subnet_create(sdk, network_id, cidr, gateway, host_routes,
+                           allocation_pool, name, segment_id, dns_nameservers):
+    try:
+        if netaddr.IPNetwork(cidr).version == 6:
+            subnet = sdk.network.create_subnet(
+                name=name,
+                cidr=cidr,
+                gateway_ip=gateway,
+                enable_dhcp=True,
+                ip_version='6',
+                ipv6_address_mode='dhcpv6-stateless',
+                ipv6_ra_mode='dhcpv6-stateless',
+                allocation_pools=allocation_pool,
+                network_id=network_id,
+                segment_id=segment_id,
+                dns_nameservers=dns_nameservers)
+        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,
+                segment_id=segment_id,
+                dns_nameservers=dns_nameservers)
+            print('INFO: Subnet created %s' % subnet)
+    except Exception:
+        print('ERROR: Create subnet %s failed.' % name)
+        raise
+
+    return subnet
+
+
+def _neutron_subnet_update(sdk, subnet_id, cidr, gateway, host_routes,
+                           allocation_pool, name, dns_nameservers):
+    try:
+        if netaddr.IPNetwork(cidr).version == 6:
+            subnet = sdk.network.update_subnet(
+                subnet_id,
+                name=name,
+                gateway_ip=gateway,
+                allocation_pools=allocation_pool,
+                dns_nameservers=dns_nameservers)
+        else:
+            subnet = sdk.network.update_subnet(
+                subnet_id,
+                name=name,
+                gateway_ip=gateway,
+                host_routes=host_routes,
+                allocation_pools=allocation_pool,
+                dns_nameservers=dns_nameservers)
+        print('INFO: Subnet updated %s' % subnet)
+    except Exception:
+        print('ERROR: Update of subnet %s failed.' % name)
+        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')
+        print('INFO: Neutron Segment created %s' % segment)
+    except Exception as ex:
+        print('ERROR: Neutron Segment %s create failed.' % name)
+        raise
+
+    return segment
+
+
+def _neutron_segment_update(sdk, segment_id, name):
+    try:
+        segment = sdk.network.update_segment(segment_id, name=name)
+        print('INFO: Neutron Segment updated %s', segment)
+    except Exception:
+        print('ERROR: Neutron Segment %s update failed.' % name)
+        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:
+        print('ERROR: Create router for subnet %s failed.' % name)
+        raise
+
+
+def _get_subnet(sdk, cidr, network_id):
+    try:
+        subnet = list(sdk.network.subnets(cidr=cidr, network_id=network_id))
+    except Exception as ex:
+        print('ERROR: Get subnet with cidr %s failed.' % cidr)
+        raise
+
+    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:
+        print('ERROR: Get segment for physical_network %s on network_id %s '
+              'failed.' % (phy, network_id))
+        raise
+
+    return False if not segment else segment[0]
+
+
+def config_neutron_segments_and_subnets(sdk, ctlplane_id):
+    s = CONF['subnets'][CONF['local_subnet']]
+    subnet = _get_subnet(sdk, s['NetworkCidr'], ctlplane_id)
+    if subnet and not subnet.segment_id:
+        print('WARNING: 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': CONF['local_ip']}]
+        allocation_pool = [{'start': s['DhcpRangeStart'],
+                            'end': s['DhcpRangeEnd']}]
+        _neutron_subnet_update(
+            sdk, subnet.id, s['NetworkCidr'], s['NetworkGateway'], host_routes,
+            allocation_pool, CONF['local_subnet'], CONF['nameservers'])
+        # 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 netaddr.IPNetwork(s['NetworkCidr']).version == 6:
+            _ensure_neutron_router(sdk, CONF['local_subnet'], subnet.id)
+    else:
+        for name in CONF['subnets']:
+            s = CONF['subnets'][name]
+
+            phynet = name
+            metadata_nexthop = s['NetworkGateway']
+            if name == CONF['local_subnet']:
+                phynet = CONF['physical_network']
+                metadata_nexthop = CONF['local_ip']
+
+            host_routes = [{'destination': '169.254.169.254/32',
+                            'nexthop': metadata_nexthop}]
+            allocation_pool = [{'start': s['DhcpRangeStart'],
+                                'end': s['DhcpRangeEnd']}]
+
+            subnet = _get_subnet(sdk, s['NetworkCidr'], 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)):
+                    raise RuntimeError(
+                        'The cidr: %s of the local subnet is already used in '
+                        'subnet: %s which is associated with segment_id: %s.' %
+                        (s['NetworkCidr'], subnet.id, subnet.segment_id))
+
+            if subnet:
+                _neutron_segment_update(sdk, subnet.segment_id, name)
+                _neutron_subnet_update(
+                    sdk, subnet.id, s['NetworkCidr'], s['NetworkGateway'],
+                    host_routes, allocation_pool, name, CONF['nameservers'])
+            else:
+                if segment:
+                    _neutron_segment_update(sdk, segment.id, name)
+                else:
+                    segment = _neutron_segment_create(sdk, name,
+                                                      ctlplane_id, phynet)
+
+                if CONF['enable_routed_networks']:
+                    subnet = _neutron_subnet_create(
+                        sdk, ctlplane_id, s['NetworkCidr'],
+                        s['NetworkGateway'], host_routes, allocation_pool,
+                        name, segment.id, CONF['nameservers'])
+                else:
+                    subnet = _neutron_subnet_create(
+                        sdk, ctlplane_id, s['NetworkCidr'],
+                        s['NetworkGateway'], host_routes, allocation_pool,
+                        name, None, CONF['nameservers'])
+
+            # 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 netaddr.IPNetwork(s['NetworkCidr']).version == 6:
+                _ensure_neutron_router(sdk, name, subnet.id)
+
+
+
+if _run_command(['hiera', 'neutron_api_enabled'], name='hiera'):
+    sdk = os_client_config.make_sdk(auth_url=AUTH_URL,
+                                    project_name='admin',
+                                    username='admin',
+                                    password=ADMIN_PASSWORD,
+                                    project_domain_name='Default',
+                                    user_domain_name='Default')
+
+    network = _ensure_neutron_network(sdk)
+    config_neutron_segments_and_subnets(sdk, network.id)
diff --git a/extraconfig/post_deploy/undercloud_post.sh b/extraconfig/post_deploy/undercloud_post.sh
index 16f30517ca..6c2368aec8 100755
--- a/extraconfig/post_deploy/undercloud_post.sh
+++ b/extraconfig/post_deploy/undercloud_post.sh
@@ -64,57 +64,6 @@ if ! grep "$(cat $HOMEDIR/.ssh/id_rsa.pub)" $HOMEDIR/.ssh/authorized_keys; then
 fi
 chown -R "$USERNAME:$GROUPNAME" "$HOMEDIR/.ssh"
 
-if [ "$(hiera neutron_api_enabled)" = "true" ]; then
-    PHYSICAL_NETWORK=ctlplane
-
-    ctlplane_id=$(openstack network list -f csv -c ID -c Name --quote none | tail -n +2 | grep ctlplane | cut -d, -f1)
-    subnet_ids=$(openstack subnet list -f csv -c ID --quote none | tail -n +2)
-    subnet_id=
-
-    for subnet_id in $subnet_ids; do
-        network_id=$(openstack subnet show -f value -c network_id $subnet_id)
-        if [ "$network_id" = "$ctlplane_id" ]; then
-            break
-        fi
-    done
-
-    net_create=1
-    if [ -n "$subnet_id" ]; then
-        cidr=$(openstack subnet show $subnet_id -f value -c cidr)
-        if [ "$cidr" = "$undercloud_network_cidr" ]; then
-            net_create=0
-        else
-            echo "New cidr $undercloud_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 subnet and network to make sure it doesn't already exist
-        if openstack subnet list | grep start; then
-            openstack subnet delete $(openstack subnet list | grep start | awk '{print $4}')
-        fi
-        if openstack network show ctlplane; then
-            openstack network delete ctlplane
-        fi
-
-
-        NETWORK_ID=$(openstack network create --provider-network-type=flat --provider-physical-network=ctlplane ctlplane | grep " id " | awk '{print $4}')
-
-        NAMESERVER_ARG=""
-        if [ -n "${undercloud_nameserver:-}" ]; then
-            NAMESERVER_ARG="--dns-nameserver $undercloud_nameserver"
-        fi
-
-        openstack subnet create --network=$NETWORK_ID \
-            --gateway=$undercloud_network_gateway \
-            --subnet-range=$undercloud_network_cidr \
-            --allocation-pool start=$undercloud_dhcp_start,end=$undercloud_dhcp_end \
-            --host-route destination=169.254.169.254/32,gateway=$local_ip \
-            $NAMESERVER_ARG ctlplane-subnet
-    fi
-fi
-
 if [ "$(hiera nova_api_enabled)" = "true" ]; then
     # Disable nova quotas
     openstack quota set --cores -1 --instances -1 --ram -1 $(openstack project show admin | awk '$2=="id" {print $4}')
diff --git a/extraconfig/post_deploy/undercloud_post.yaml b/extraconfig/post_deploy/undercloud_post.yaml
index b0e13f3a17..e83ff11a5d 100644
--- a/extraconfig/post_deploy/undercloud_post.yaml
+++ b/extraconfig/post_deploy/undercloud_post.yaml
@@ -13,21 +13,6 @@ parameters:
     description: The HOME directory where the stackrc and ssh credentials for the Undercloud will be installed. Set to /home/<user> to customize the location.
     type: string
     default: '/root'
-  UndercloudDhcpRangeStart:
-    type: string
-    default: '192.168.24.5'
-  UndercloudDhcpRangeEnd:
-    type: string
-    default: '192.168.24.24'
-  UndercloudNetworkCidr:
-    type: string
-    default: '192.168.24.0/24'
-  UndercloudNetworkGateway:
-    type: string
-    default: '192.168.24.1'
-  UndercloudNameserver:
-    type: string
-    default: ''
   AdminPassword: #supplied by tripleo-undercloud-passwords.yaml
     type: string
     description: The password for the keystone admin account, used for monitoring, querying neutron etc.
@@ -52,6 +37,33 @@ parameters:
     description: >
       Whether the TripleO validations are enabled.
     type: boolean
+  DnsServers: # Override this via parameter_defaults
+    default: []
+    description: A list of DNS servers (2 max for some implementations) that will be added to resolv.conf.
+    type: comma_delimited_list
+  CtlplaneLocalPhysicalNetwork:
+    default: ctlplane
+    type: string
+    description: Physical network name for the ctlplane network local to the undercloud
+  UndercloudCtlplaneSubnets:
+    description: >
+      Dictionary of subnets to configure on the Undercloud ctlplan network
+    default: {}
+    type: json
+  UndercloudCtlplaneLocalSubnet:
+    description: The subnet local to the undercloud on the ctlplane network
+    default: ctlplane-subnet
+    type: string
+  UndercloudEnableRoutedNetworks:
+    description: Enable support for routed ctlplane networks.
+    default: False
+    type: boolean
+  UndercloudLocalMtu: # Override this via parameter_defaults
+    default: 1500
+    description: MTU to use for the Undercloud local_interface.
+    type: number
+    constraints:
+      - range: { min: 1000, max: 65536 }
 
 conditions:
 
@@ -74,12 +86,6 @@ resources:
       group: script
       inputs:
         - name: deploy_identifier
-        - name: local_ip
-        - name: undercloud_dhcp_start
-        - name: undercloud_dhcp_end
-        - name: undercloud_network_cidr
-        - name: undercloud_network_gateway
-        - name: undercloud_nameserver
         - name: admin_password
         - name: auth_url
         - name: snmp_readonly_user_password
@@ -93,12 +99,6 @@ resources:
       servers: {get_param: servers}
       config: {get_resource: UndercloudPostConfig}
       input_values:
-        local_ip: {get_param: [DeployedServerPortMap, 'control_virtual_ip', fixed_ips, 0, ip_address]}
-        undercloud_dhcp_start: {get_param: UndercloudDhcpRangeStart}
-        undercloud_dhcp_end: {get_param: UndercloudDhcpRangeEnd}
-        undercloud_network_cidr: {get_param: UndercloudNetworkCidr}
-        undercloud_network_gateway: {get_param: UndercloudNetworkGateway}
-        undercloud_nameserver: {get_param: UndercloudNameserver}
         ssl_certificate: {get_param: SSLCertificate}
         homedir: {get_param: UndercloudHomeDir}
         admin_password: {get_param: AdminPassword}
@@ -118,3 +118,48 @@ resources:
               host: {get_param: [DeployedServerPortMap, 'control_virtual_ip', fixed_ips, 0, ip_address]}
               port: 5000
               path: /
+
+  UndercloudCtlplaneNetworkConfig:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: script
+      inputs:
+        - name: admin_password
+        - name: auth_url
+        - name: config
+      config: {get_file: ./undercloud_ctlplane_network.py}
+
+  UndercloudCtlplaneNetworkDeployment:
+    type: OS::Heat::SoftwareDeployments
+    properties:
+      name: UndercloudCtlplaneNetworkDeployment
+      servers: {get_param: servers}
+      config: {get_resource: UndercloudCtlplaneNetworkConfig}
+      input_values:
+        admin_password: {get_param: AdminPassword}
+        # if SSL is enabled we use the public virtual ip as the stackrc endpoint
+        auth_url:
+          if:
+          - tls_enabled
+          - make_url:
+              scheme: https
+              host: {get_param: [DeployedServerPortMap, 'public_virtual_ip', fixed_ips, 0, ip_address]}
+              port: 13000
+              path: /
+          - make_url:
+              scheme: http
+              host: {get_param: [DeployedServerPortMap, 'control_virtual_ip', fixed_ips, 0, ip_address]}
+              port: 5000
+              path: /
+        config:
+          str_replace:
+            template: JSON
+            params:
+              JSON:
+                local_ip: {get_param: [DeployedServerPortMap, 'control_virtual_ip', fixed_ips, 0, ip_address]}
+                local_subnet: {get_param: UndercloudCtlplaneLocalSubnet}
+                nameservers: {get_param: DnsServers}
+                physical_network: {get_param: CtlplaneLocalPhysicalNetwork}
+                subnets: {get_param: UndercloudCtlplaneSubnets}
+                enable_routed_networks: {get_param: UndercloudEnableRoutedNetworks}
+                mtu: {get_param: UndercloudLocalMtu}
diff --git a/net-config-undercloud.j2.yaml b/net-config-undercloud.j2.yaml
index 03b0b6795a..9341609aa2 100644
--- a/net-config-undercloud.j2.yaml
+++ b/net-config-undercloud.j2.yaml
@@ -29,6 +29,8 @@ parameters:
     default: 1500
     description: MTU to use for the Undercloud local_interface.
     type: number
+    constraints:
+      - range: { min: 1000, max: 65536 }
 resources:
   OsNetConfigImpl:
     type: OS::Heat::SoftwareConfig