diff --git a/devstack/lib/dns b/devstack/lib/dns index efa00e39cd1..8c8b5648b39 100644 --- a/devstack/lib/dns +++ b/devstack/lib/dns @@ -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 diff --git a/doc/source/contributor/internals/external_dns_integration.rst b/doc/source/contributor/internals/external_dns_integration.rst index 0c9f7fc923d..4261fc67c4a 100644 --- a/doc/source/contributor/internals/external_dns_integration.rst +++ b/doc/source/contributor/internals/external_dns_integration.rst @@ -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: ````, +````, ````, ````. 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 ".demo.net." + +---------------------------+--------------------------------------+ + | Field | Value | + +---------------------------+--------------------------------------+ + | admin_state_up | UP | + | availability_zone_hints | | + | availability_zones | | + | created_at | 2021-02-19T15:16:36Z | + | description | | + | dns_domain | .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 | + +--------------------------------------+-------------------------------------------------------------+------+------------------------------------------------------------------------------------------------------+--------+--------+ diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 9f2e8955d6f..7f9084ab235 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -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 @@ -70,6 +71,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, diff --git a/neutron/extensions/dns_integration_domain_keywords.py b/neutron/extensions/dns_integration_domain_keywords.py new file mode 100644 index 00000000000..122919e0385 --- /dev/null +++ b/neutron/extensions/dns_integration_domain_keywords.py @@ -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 diff --git a/neutron/plugins/ml2/extensions/dns_domain_keywords.py b/neutron/plugins/ml2/extensions/dns_domain_keywords.py new file mode 100644 index 00000000000..35bdc66fa9e --- /dev/null +++ b/neutron/plugins/ml2/extensions/dns_domain_keywords.py @@ -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 diff --git a/neutron/plugins/ml2/extensions/dns_integration.py b/neutron/plugins/ml2/extensions/dns_integration.py index 2bf79f79a52..280565804a9 100644 --- a/neutron/plugins/ml2/extensions/dns_integration.py +++ b/neutron/plugins/ml2/extensions/dns_integration.py @@ -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] diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index f7be30865fe..201faf89e36 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -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 @@ -106,6 +107,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): diff --git a/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py b/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py new file mode 100644 index 00000000000..9fa17b0bdab --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py @@ -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] = ( + ".%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 = [ + ("....domain", + "%s.%s.%s.%s.domain" % (ctx.project_id, ctx.project_name, + ctx.user_id, ctx.user_name)), + (".domain", + "%s.domain" % ctx.project_id), + (".domain", + "%s.domain" % ctx.project_name), + (".domain", + "%s.domain" % ctx.user_id), + (".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 = "....domain" + ctx = context.Context( + project_id=uuidutils.generate_uuid(), + project_name=None, + user_id=uuidutils.generate_uuid(), + user_name="user" + ) + expected_domain = "%s..%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']) diff --git a/releasenotes/notes/dns-domain-per-tenant-59b8a368fa06f81d.yaml b/releasenotes/notes/dns-domain-per-tenant-59b8a368fa06f81d.yaml new file mode 100644 index 00000000000..6dc78b03f75 --- /dev/null +++ b/releasenotes/notes/dns-domain-per-tenant-59b8a368fa06f81d.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Special keywords ````, ````, ```` + and ```` 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. diff --git a/setup.cfg b/setup.cfg index b8d3ecc4519..d32e27030ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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