Merge "Allow to parse keywords in dns labels"

This commit is contained in:
Zuul 2021-06-22 13:31:32 +00:00 committed by Gerrit Code Review
commit e431c09438
10 changed files with 491 additions and 8 deletions

View File

@ -1,5 +1,5 @@
function configure_dns_extension {
neutron_ml2_extension_driver_add "subnet_dns_publish_fixed_ip"
neutron_ml2_extension_driver_add "dns_domain_keywords"
}
function configure_dns_integration {
iniset $NEUTRON_CONF DEFAULT external_dns_driver designate

View File

@ -41,3 +41,166 @@ Specifically, floating ips, ports and networks are extended as follows:
* Floating ips have a *dns_name* and a *dns_domain* attribute.
* Ports have a *dns_name* attribute.
* Networks have a *dns_domain* attributes.
Pre-configured domains for projects and users
---------------------------------------------
ML2 plugin extension ``dns_domain_keywords`` provides same dns integration as
``dns_domain_ports`` and ``subnet_dns_publish_fixed_ip`` and it also allows to
configure network's dns_domain with some specific keywords: ``<project_id>``,
``<project_name>``, ``<user_id>``, ``<user_name>``. Please see example below for
more details.
* Create DNS zone. ``0511951bd56e4a0aac27ac65e00bddd0`` is ID of the project
used in the example
.. code-block:: console
$ openstack zone create 0511951bd56e4a0aac27ac65e00bddd0.example.com. --email admin@0511951bd56e4a0aac27ac65e00bddd0.example.com
+----------------+----------------------------------------------------+
| Field | Value |
+----------------+----------------------------------------------------+
| action | CREATE |
| attributes | |
| created_at | 2021-02-19T14:48:06.000000 |
| description | None |
| email | admin@0511951bd56e4a0aac27ac65e00bddd0.example.com |
| id | c14a8edc-d0b9-4cdd-93f1-1ab5a5f5ff9d |
| masters | |
| name | 0511951bd56e4a0aac27ac65e00bddd0.example.com. |
| pool_id | 794ccc2c-d751-44fe-b57f-8894c9f5c842 |
| project_id | 0511951bd56e4a0aac27ac65e00bddd0 |
| serial | 1613746085 |
| status | PENDING |
| transferred_at | None |
| ttl | 3600 |
| type | PRIMARY |
| updated_at | None |
| version | 1 |
+----------------+----------------------------------------------------+
* Create network with dns_domain
.. code-block:: console
$ openstack network create dns-test-network --dns-domain "<project_id>.demo.net."
+---------------------------+--------------------------------------+
| Field | Value |
+---------------------------+--------------------------------------+
| admin_state_up | UP |
| availability_zone_hints | |
| availability_zones | |
| created_at | 2021-02-19T15:16:36Z |
| description | |
| dns_domain | <project_id>.demo.net. |
| id | fb247287-43aa-4a83-b768-a3b34dc6735a |
| ipv4_address_scope | None |
| ipv6_address_scope | None |
| is_default | False |
| is_vlan_transparent | None |
| mtu | 1450 |
| name | dns-test-network |
| port_security_enabled | True |
| project_id | 0511951bd56e4a0aac27ac65e00bddd0 |
| provider:network_type | vxlan |
| provider:physical_network | None |
| provider:segmentation_id | 1003 |
| qos_policy_id | None |
| revision_number | 1 |
| router:external | Internal |
| segments | None |
| shared | False |
| status | ACTIVE |
| subnets | |
| tags | |
| updated_at | 2021-02-19T15:16:37Z |
+---------------------------+--------------------------------------+
$ openstack subnet create --network dns-test-network --subnet-range 192.168.100.0/24 --dns-publish-fixed-ip dns-test-subnet
+----------------------+--------------------------------------+
| Field | Value |
+----------------------+--------------------------------------+
| allocation_pools | 192.168.100.2-192.168.100.254 |
| cidr | 192.168.100.0/24 |
| created_at | 2021-02-19T15:21:50Z |
| description | |
| dns_nameservers | |
| dns_publish_fixed_ip | True |
| enable_dhcp | True |
| gateway_ip | 192.168.100.1 |
| host_routes | |
| id | 2547a3f2-374f-4262-aed5-3a69af73e732 |
| ip_version | 4 |
| ipv6_address_mode | None |
| ipv6_ra_mode | None |
| name | dns-test-subnet |
| network_id | fb247287-43aa-4a83-b768-a3b34dc6735a |
| prefix_length | None |
| project_id | 0511951bd56e4a0aac27ac65e00bddd0 |
| revision_number | 0 |
| segment_id | None |
| service_types | |
| subnetpool_id | None |
| tags | |
| updated_at | 2021-02-19T15:21:50Z |
+----------------------+--------------------------------------+
* Create port in that network
.. code-block:: console
$ openstack port create --network dns-test-network --dns-name dns-test-port test-port
+-------------------------+---------------------------------------------------------------------------------------------------------------------------+
| Field | Value |
+-------------------------+---------------------------------------------------------------------------------------------------------------------------+
| admin_state_up | UP |
| allowed_address_pairs | |
| binding_host_id | |
| binding_profile | |
| binding_vif_details | |
| binding_vif_type | unbound |
| binding_vnic_type | normal |
| created_at | 2021-02-19T15:22:51Z |
| data_plane_status | None |
| description | |
| device_id | |
| device_owner | |
| device_profile | None |
| dns_assignment | fqdn='dns-test-port.0511951bd56e4a0aac27ac65e00bddd0.example.com.', hostname='dns-test-port', ip_address='192.168.100.17' |
| dns_domain | |
| dns_name | dns-test-port |
| extra_dhcp_opts | |
| fixed_ips | ip_address='192.168.100.17', subnet_id='2547a3f2-374f-4262-aed5-3a69af73e732' |
| id | f30908a1-6ef5-4137-bff4-c1205c6660ee |
| ip_allocation | None |
| mac_address | fa:16:3e:e8:33:b8 |
| name | test-port |
| network_id | fb247287-43aa-4a83-b768-a3b34dc6735a |
| numa_affinity_policy | None |
| port_security_enabled | True |
| project_id | 0511951bd56e4a0aac27ac65e00bddd0 |
| propagate_uplink_status | None |
| qos_network_policy_id | None |
| qos_policy_id | None |
| resource_request | None |
| revision_number | 1 |
| security_group_ids | 4425c3fd-6705-4134-9878-07b333d81314 |
| status | DOWN |
| tags | |
| trunk_details | None |
| updated_at | 2021-02-19T15:22:51Z |
+-------------------------+---------------------------------------------------------------------------------------------------------------------------+
* Test if recordset was created properly in the DNS zone
.. code-block:: console
$ openstack recordset list c14a8edc-d0b9-4cdd-93f1-1ab5a5f5ff9d
+--------------------------------------+-------------------------------------------------------------+------+------------------------------------------------------------------------------------------------------+--------+--------+
| id | name | type | records | status | action |
+--------------------------------------+-------------------------------------------------------------+------+------------------------------------------------------------------------------------------------------+--------+--------+
| 1c302468-4e30-466e-9330-e4cd9191ff99 | 0511951bd56e4a0aac27ac65e00bddd0.example.com. | SOA | ns1.devstack.org. admin.0511951bd56e4a0aac27ac65e00bddd0.example.com. 1613748171 3549 600 86400 3600 | ACTIVE | NONE |
| 99ce92d1-8c7a-4193-aeb2-44835048a6fa | 0511951bd56e4a0aac27ac65e00bddd0.example.com. | NS | ns1.devstack.org. | ACTIVE | NONE |
| 01f0569d-ce81-4424-915f-c6fe6229256e | dns-test-port.0511951bd56e4a0aac27ac65e00bddd0.example.com. | A | 192.168.100.17 | ACTIVE | NONE |
+--------------------------------------+-------------------------------------------------------------+------+------------------------------------------------------------------------------------------------------+--------+--------+

View File

@ -18,6 +18,7 @@ from neutron_lib.api.definitions import auto_allocated_topology
from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib.api.definitions import default_subnetpools
from neutron_lib.api.definitions import dns
from neutron_lib.api.definitions import dns_domain_keywords
from neutron_lib.api.definitions import expose_port_forwarding_in_fip
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import extra_dhcp_opt
@ -74,6 +75,7 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [
sorting.ALIAS,
project_id.ALIAS,
dns.ALIAS,
dns_domain_keywords.ALIAS,
agent_def.ALIAS,
az_def.ALIAS,
raz_def.ALIAS,

View File

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import dns_domain_keywords as apidef
from neutron_lib.api import extensions
class Dns_integration_domain_keywords(extensions.APIExtensionDescriptor):
"""Extension class supporting configuration of dns domain with keywords."""
api_definition = apidef

View File

@ -0,0 +1,46 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import dns_domain_keywords
from neutron_lib.api.definitions import dns_domain_ports as ports_apidef
from neutron_lib.api.definitions import subnet_dns_publish_fixed_ip as sn_dns
from neutron_lib import constants as lib_const
from oslo_log import log as logging
from neutron.plugins.ml2.extensions import subnet_dns_publish_fixed_ip
LOG = logging.getLogger(__name__)
class DnsDomainKeywordsExtensionDriver(
subnet_dns_publish_fixed_ip.SubnetDNSPublishFixedIPExtensionDriver):
_supported_extension_aliases = [dns_apidef.ALIAS,
ports_apidef.ALIAS,
sn_dns.ALIAS,
dns_domain_keywords.ALIAS]
def initialize(self):
LOG.info("DnsDomainKeywordsExtensionDriver initialization complete")
@staticmethod
def _parse_dns_domain(plugin_context, domain):
for keyword in lib_const.DNS_LABEL_KEYWORDS:
keyword_value = getattr(plugin_context, keyword, None)
if keyword_value is not None:
domain = domain.replace('<' + keyword + '>', keyword_value)
else:
LOG.warning("Keyword <%s> does not have value in current "
"context and it will not be replaced in the "
"domain %s", keyword, domain)
return domain

View File

@ -43,6 +43,10 @@ class DNSExtensionDriver(api.ExtensionDriver):
def extension_alias(self):
return self._supported_extension_alias
@staticmethod
def _parse_dns_domain(plugin_context, domain):
return domain
def process_create_network(self, plugin_context, request_data, db_data):
dns_domain = request_data.get(dns_apidef.DNSDOMAIN)
if not validators.is_attr_set(dns_domain):
@ -101,7 +105,7 @@ class DNSExtensionDriver(api.ExtensionDriver):
flag = self.external_dns_not_needed(plugin_context, network, subnets)
current_dns_name, current_dns_domain = (
self._calculate_current_dns_name_and_domain(
dns_name, external_dns_domain, flag))
plugin_context, dns_name, external_dns_domain, flag))
dns_data_obj = port_obj.PortDNS(
plugin_context,
@ -115,7 +119,7 @@ class DNSExtensionDriver(api.ExtensionDriver):
dns_data_obj.create()
return dns_data_obj
def _calculate_current_dns_name_and_domain(self, dns_name,
def _calculate_current_dns_name_and_domain(self, plugin_context, dns_name,
external_dns_domain,
no_external_dns_service):
# When creating a new PortDNS object, the current_dns_name and
@ -131,7 +135,8 @@ class DNSExtensionDriver(api.ExtensionDriver):
are_both_dns_attributes_set = dns_name and external_dns_domain
if no_external_dns_service or not are_both_dns_attributes_set:
return '', ''
return dns_name, external_dns_domain
return dns_name, self._parse_dns_domain(
plugin_context, external_dns_domain)
def _update_dns_db(self, plugin_context, request_data, db_data, network,
subnets):
@ -153,7 +158,8 @@ class DNSExtensionDriver(api.ExtensionDriver):
dns_data_db = self._populate_previous_external_dns_data(
dns_data_db)
dns_data_db = self._populate_current_external_dns_data(
request_data, network, dns_data_db, dns_name, dns_domain,
plugin_context, request_data,
network, dns_data_db, dns_name, dns_domain,
is_dns_name_changed, is_dns_domain_changed)
elif not dns_data_db['current_dns_name']:
# If port was removed from external DNS service in previous
@ -176,15 +182,17 @@ class DNSExtensionDriver(api.ExtensionDriver):
dns_data_db['current_dns_domain'])
return dns_data_db
def _populate_current_external_dns_data(self, request_data, network,
dns_data_db, dns_name, dns_domain,
is_dns_name_changed,
def _populate_current_external_dns_data(self, plugin_context, request_data,
network, dns_data_db, dns_name,
dns_domain, is_dns_name_changed,
is_dns_domain_changed):
if is_dns_name_changed or is_dns_domain_changed:
if is_dns_name_changed:
dns_data_db[dns_apidef.DNSNAME] = dns_name
external_dns_domain = (dns_data_db[dns_apidef.DNSDOMAIN] or
network.get(dns_apidef.DNSDOMAIN))
external_dns_domain = self._parse_dns_domain(
plugin_context, external_dns_domain)
if is_dns_domain_changed:
dns_data_db[dns_apidef.DNSDOMAIN] = dns_domain
external_dns_domain = request_data[dns_apidef.DNSDOMAIN]

View File

@ -13,6 +13,7 @@
#
from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import dns_domain_keywords
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet
@ -107,6 +108,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
if not api_extensions.is_extension_supported(
core_plugin, dns_apidef.ALIAS):
aliases.remove(dns_apidef.ALIAS)
aliases.remove(dns_domain_keywords.ALIAS)
@property
def supported_extension_aliases(self):

View File

@ -0,0 +1,226 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import provider_net as pnet
from neutron_lib import context
from oslo_utils import uuidutils
from neutron.objects import ports as port_obj
from neutron.plugins.ml2.extensions import dns_domain_keywords
from neutron.tests.unit.plugins.ml2.extensions import test_dns_integration
PROJECT_ID = uuidutils.generate_uuid()
class DNSDomainKeyworkdsTestCase(
test_dns_integration.DNSIntegrationTestCase):
_extension_drivers = ['dns_domain_keywords']
_expected_dns_domain = "%s.%s" % (PROJECT_ID,
test_dns_integration.DNSDOMAIN)
def _create_port_for_test(self, provider_net=True, dns_domain=True,
dns_name=True, ipv4=True, ipv6=True,
dns_domain_port=False):
net_kwargs = {}
if provider_net:
net_kwargs = {
'arg_list': (pnet.NETWORK_TYPE, pnet.SEGMENTATION_ID,),
pnet.NETWORK_TYPE: 'vxlan',
pnet.SEGMENTATION_ID: '2016',
}
if dns_domain:
net_kwargs[dns_apidef.DNSDOMAIN] = (
"<project_id>.%s" % test_dns_integration.DNSDOMAIN)
net_kwargs['arg_list'] = \
net_kwargs.get('arg_list', ()) + (dns_apidef.DNSDOMAIN,)
net_kwargs['shared'] = True
res = self._create_network(self.fmt, 'test_network', True,
**net_kwargs)
network = self.deserialize(self.fmt, res)
if ipv4:
cidr = '10.0.0.0/24'
self._create_subnet_for_test(network['network']['id'], cidr)
if ipv6:
cidr = 'fd3d:bdd4:da60::/64'
self._create_subnet_for_test(network['network']['id'], cidr)
port_kwargs = {}
if dns_name:
port_kwargs = {
'arg_list': (dns_apidef.DNSNAME,),
dns_apidef.DNSNAME: test_dns_integration.DNSNAME
}
if dns_domain_port:
port_kwargs[dns_apidef.DNSDOMAIN] = (
test_dns_integration.PORTDNSDOMAIN)
port_kwargs['arg_list'] = (port_kwargs.get('arg_list', ()) +
(dns_apidef.DNSDOMAIN,))
res = self._create_port('json', network['network']['id'],
set_context=True, tenant_id=PROJECT_ID,
**port_kwargs)
self.assertEqual(201, res.status_int)
port = self.deserialize(self.fmt, res)['port']
ctx = context.get_admin_context()
dns_data_db = port_obj.PortDNS.get_object(ctx, port_id=port['id'])
return port, dns_data_db
def _update_port_for_test(self, port,
new_dns_name=test_dns_integration.NEWDNSNAME,
new_dns_domain=None, **kwargs):
test_dns_integration.mock_client.reset_mock()
ip_addresses = [netaddr.IPAddress(ip['ip_address'])
for ip in port['fixed_ips']]
records_v4 = [ip for ip in ip_addresses if ip.version == 4]
records_v6 = [ip for ip in ip_addresses if ip.version == 6]
recordsets = []
if records_v4:
recordsets.append({'id': test_dns_integration.V4UUID,
'records': records_v4})
if records_v6:
recordsets.append({'id': test_dns_integration.V6UUID,
'records': records_v6})
test_dns_integration.mock_client.recordsets.list.return_value = (
recordsets)
test_dns_integration.mock_admin_client.reset_mock()
body = {}
if new_dns_name is not None:
body['dns_name'] = new_dns_name
if new_dns_domain is not None:
body[dns_apidef.DNSDOMAIN] = new_dns_domain
body.update(kwargs)
data = {'port': body}
# NOTE(slaweq): Admin context is required here to be able to update
# fixed_ips of the port as by default it is not possible for non-admin
# users
ctx = context.Context(project_id=PROJECT_ID, is_admin=True)
req = self.new_update_request('ports', data, port['id'], context=ctx)
res = req.get_response(self.api)
self.assertEqual(200, res.status_int)
port = self.deserialize(self.fmt, res)['port']
admin_ctx = context.get_admin_context()
dns_data_db = port_obj.PortDNS.get_object(admin_ctx,
port_id=port['id'])
return port, dns_data_db
def _verify_port_dns(self, port, dns_data_db, dns_name=True,
dns_domain=True, ptr_zones=True, delete_records=False,
provider_net=True, dns_driver=True, original_ips=None,
current_dns_name=test_dns_integration.DNSNAME,
previous_dns_name='', dns_domain_port=False,
current_dns_domain=None, previous_dns_domain=None):
current_dns_domain = current_dns_domain or self._expected_dns_domain
previous_dns_domain = previous_dns_domain or self._expected_dns_domain
super(DNSDomainKeyworkdsTestCase, self)._verify_port_dns(
port=port, dns_data_db=dns_data_db, dns_name=dns_name,
dns_domain=dns_domain, ptr_zones=ptr_zones,
delete_records=delete_records, provider_net=provider_net,
dns_driver=dns_driver, original_ips=original_ips,
current_dns_name=current_dns_name,
previous_dns_name=previous_dns_name,
dns_domain_port=dns_domain_port,
current_dns_domain=current_dns_domain,
previous_dns_domain=previous_dns_domain)
def test__parse_dns_domain(self, *mocks):
ctx = context.Context(
project_id=uuidutils.generate_uuid(),
project_name="project",
user_id=uuidutils.generate_uuid(),
user_name="user"
)
domains = [
("<project_id>.<project_name>.<user_id>.<user_name>.domain",
"%s.%s.%s.%s.domain" % (ctx.project_id, ctx.project_name,
ctx.user_id, ctx.user_name)),
("<project_id>.domain",
"%s.domain" % ctx.project_id),
("<project_name>.domain",
"%s.domain" % ctx.project_name),
("<user_id>.domain",
"%s.domain" % ctx.user_id),
("<user_name>.domain",
"%s.domain" % ctx.user_name)]
for domain, expected_domain in domains:
self.assertEqual(
expected_domain,
dns_domain_keywords.DnsDomainKeywordsExtensionDriver.
_parse_dns_domain(ctx, domain))
def test__parse_dns_domain_missing_fields_in_context(self, *mocks):
domain = "<project_id>.<project_name>.<user_id>.<user_name>.domain"
ctx = context.Context(
project_id=uuidutils.generate_uuid(),
project_name=None,
user_id=uuidutils.generate_uuid(),
user_name="user"
)
expected_domain = "%s.<project_name>.%s.%s.domain" % (
ctx.project_id, ctx.user_id, ctx.user_name)
self.assertEqual(
expected_domain,
dns_domain_keywords.DnsDomainKeywordsExtensionDriver.
_parse_dns_domain(ctx, domain))
def test_update_port_with_current_dns_name(self, *mocks):
port, dns_data_db = self._create_port_for_test()
port, dns_data_db = self._update_port_for_test(
port, new_dns_name=test_dns_integration.DNSNAME)
self.assertEqual(test_dns_integration.DNSNAME,
dns_data_db['current_dns_name'])
self.assertEqual(self._expected_dns_domain,
dns_data_db['current_dns_domain'])
self.assertEqual('', dns_data_db['previous_dns_name'])
self.assertEqual('', dns_data_db['previous_dns_domain'])
self.assertFalse(
test_dns_integration.mock_client.recordsets.create.call_args_list)
self.assertFalse(
test_dns_integration.mock_admin_client.recordsets.
create.call_args_list)
self.assertFalse(
test_dns_integration.mock_client.recordsets.delete.call_args_list)
self.assertFalse(
test_dns_integration.mock_admin_client.recordsets.
delete.call_args_list)
def test_update_port_non_dns_name_attribute(self, *mocks):
port, dns_data_db = self._create_port_for_test()
port_name = 'port_name'
kwargs = {'name': port_name}
port, dns_data_db = self._update_port_for_test(port,
new_dns_name=None,
**kwargs)
self.assertEqual(test_dns_integration.DNSNAME,
dns_data_db['current_dns_name'])
self.assertEqual(self._expected_dns_domain,
dns_data_db['current_dns_domain'])
self.assertEqual('', dns_data_db['previous_dns_name'])
self.assertEqual('', dns_data_db['previous_dns_domain'])
self.assertFalse(
test_dns_integration.mock_client.recordsets.create.call_args_list)
self.assertFalse(
test_dns_integration.mock_admin_client.recordsets.
create.call_args_list)
self.assertFalse(
test_dns_integration.mock_client.recordsets.delete.call_args_list)
self.assertFalse(
test_dns_integration.mock_admin_client.recordsets.
delete.call_args_list)
self.assertEqual(port_name, port['name'])

View File

@ -0,0 +1,15 @@
---
features:
- |
Special keywords ``<project_id>``, ``<project_name>``, ``<user_name>``
and ``<user_id>`` can be used in the network's, port's and floating IP's
``dns_domain`` attribute.
Those special keywords will be replaced by the corresponding data from the
request context.
With that cloud admin can define dns_domain for shared network and ports
which belongs to the other projects in the way that each project can use
separate DNS zones which needs to be pre-created by users.
To enable this feature ``dns_domain_keywords`` ML2 plugin extension has to
be enabled in the Neutron config.
Enabling multiple dns_integration extensions at the same time leads to an
error.

View File

@ -118,6 +118,7 @@ neutron.ml2.extension_drivers =
uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver
tag_ports_during_bulk_creation = neutron.plugins.ml2.extensions.tag_ports_during_bulk_creation:TagPortsDuringBulkCreationExtensionDriver
subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver
dns_domain_keywords = neutron.plugins.ml2.extensions.dns_domain_keywords:DnsDomainKeywordsExtensionDriver
neutron.ipam_drivers =
fake = neutron.tests.unit.ipam.fake_driver:FakeDriver
internal = neutron.ipam.drivers.neutrondb_ipam.driver:NeutronDbPool