diff --git a/devstack/lib/vmware_nsx_v b/devstack/lib/vmware_nsx_v index bae036066b..7518741e57 100644 --- a/devstack/lib/vmware_nsx_v +++ b/devstack/lib/vmware_nsx_v @@ -91,6 +91,7 @@ function neutron_plugin_configure_service { if [[ "$NSX_L2GW_DRIVER" != "" ]]; then iniset /$Q_PLUGIN_CONF_FILE DEFAULT nsx_l2gw_driver $NSX_L2GW_DRIVER fi + iniset /$Q_PLUGIN_CONF_FILE DEFAULT nsx_extension_drivers vmware_nsxv_dns _nsxv_ini_set password "$NSXV_PASSWORD" _nsxv_ini_set user "$NSXV_USER" _nsxv_ini_set vdn_scope_id "$NSXV_VDN_SCOPE_ID" diff --git a/devstack/lib/vmware_nsx_v3 b/devstack/lib/vmware_nsx_v3 index 16305b72d4..5911359c67 100644 --- a/devstack/lib/vmware_nsx_v3 +++ b/devstack/lib/vmware_nsx_v3 @@ -165,6 +165,7 @@ function neutron_plugin_configure_service { if [[ "$NSX_L2GW_DRIVER" != "" ]]; then iniset /$Q_PLUGIN_CONF_FILE DEFAULT nsx_l2gw_driver $NSX_L2GW_DRIVER fi + iniset /$Q_PLUGIN_CONF_FILE DEFAULT nsx_extension_drivers vmware_nsxv3_dns _nsxv3_ini_set nsx_api_user $NSX_USER _nsxv3_ini_set nsx_api_password $NSX_PASSWORD _nsxv3_ini_set retries $NSX_RETRIES diff --git a/releasenotes/notes/nsx-dns-integration-extension-8260456051d61743.yaml b/releasenotes/notes/nsx-dns-integration-extension-8260456051d61743.yaml new file mode 100644 index 0000000000..784095d4cc --- /dev/null +++ b/releasenotes/notes/nsx-dns-integration-extension-8260456051d61743.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + The dns-integration extension is now supported in both NSXV and NSXV3 + plugins. It can be enabled by adding 'vmware_nsxv_dns' (for NSXV) or + 'vmware_nsxv3_dns' (for NSXV3) to the ``nsx_extension_drivers`` + configuration variable in neutron.conf file. diff --git a/setup.cfg b/setup.cfg index b1608cbe60..f8cf9d9aaf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,9 @@ neutron.qos.notification_drivers = neutron.ipam_drivers = vmware_nsxv_ipam = vmware_nsx.services.ipam.nsx_v.driver:NsxvIpamDriver vmware_nsxv3_ipam = vmware_nsx.services.ipam.nsx_v3.driver:Nsxv3IpamDriver +vmware_nsx.extension_drivers = + vmware_nsxv_dns = vmware_nsx.extension_drivers.dns_integration:DNSExtensionDriverNSXv + vmware_nsxv3_dns = vmware_nsx.extension_drivers.dns_integration:DNSExtensionDriverNSXv3 vmware_nsx.neutron.nsxv.router_type_drivers = shared = vmware_nsx.plugins.nsx_v.drivers.shared_router_driver:RouterSharedDriver distributed = vmware_nsx.plugins.nsx_v.drivers.distributed_router_driver:RouterDistributedDriver diff --git a/vmware_nsx/extension_drivers/dns_integration.py b/vmware_nsx/extension_drivers/dns_integration.py new file mode 100644 index 0000000000..6a2313a1e8 --- /dev/null +++ b/vmware_nsx/extension_drivers/dns_integration.py @@ -0,0 +1,324 @@ +# Copyright (c) 2016 IBM +# Copyright (c) 2017 VMware, Inc. +# 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. + +from neutron_lib.api import validators +from neutron_lib.plugins import directory +from oslo_config import cfg +from oslo_log import log as logging + +from neutron.extensions import dns +from neutron.objects import network as net_obj +from neutron.objects import ports as port_obj +from neutron.services.externaldns import driver + +from vmware_nsx._i18n import _LE, _LI +from vmware_nsx.common import driver_api + +LOG = logging.getLogger(__name__) +DNS_DOMAIN_DEFAULT = 'openstacklocal.' + + +class DNSExtensionDriver(driver_api.ExtensionDriver): + _supported_extension_alias = 'dns-integration' + + @property + def extension_alias(self): + return self._supported_extension_alias + + def process_create_network(self, plugin_context, request_data, db_data): + dns_domain = request_data.get(dns.DNSDOMAIN) + if not validators.is_attr_set(dns_domain): + return + + if dns_domain: + net_obj.NetworkDNSDomain(plugin_context, + network_id=db_data['id'], + dns_domain=dns_domain).create() + db_data[dns.DNSDOMAIN] = dns_domain + + def process_update_network(self, plugin_context, request_data, db_data): + new_value = request_data.get(dns.DNSDOMAIN) + if not validators.is_attr_set(new_value): + return + + current_dns_domain = db_data.get(dns.DNSDOMAIN) + if current_dns_domain == new_value: + return + + net_id = db_data['id'] + if current_dns_domain: + net_dns_domain = net_obj.NetworkDNSDomain.get_object( + plugin_context, + network_id=net_id) + if new_value: + net_dns_domain['dns_domain'] = new_value + db_data[dns.DNSDOMAIN] = new_value + net_dns_domain.update() + else: + net_dns_domain.delete() + db_data[dns.DNSDOMAIN] = '' + elif new_value: + net_obj.NetworkDNSDomain(plugin_context, + network_id=net_id, + dns_domain=new_value).create() + db_data[dns.DNSDOMAIN] = new_value + + def process_create_port(self, plugin_context, request_data, db_data): + if not request_data.get(dns.DNSNAME): + return + dns_name, is_dns_domain_default = self._get_request_dns_name( + request_data) + if is_dns_domain_default: + return + network = self._get_network(plugin_context, db_data['network_id']) + if self.external_dns_not_needed( + plugin_context, network) or not network[dns.DNSDOMAIN]: + current_dns_name = '' + current_dns_domain = '' + else: + current_dns_name = dns_name + current_dns_domain = network[dns.DNSDOMAIN] + + port_obj.PortDNS(plugin_context, + port_id=db_data['id'], + current_dns_name=current_dns_name, + current_dns_domain=current_dns_domain, + previous_dns_name='', + previous_dns_domain='', + dns_name=dns_name).create() + + def _update_dns_db(self, dns_name, dns_domain, db_data, + plugin_context, has_fixed_ips): + dns_data_db = port_obj.PortDNS.get_object( + plugin_context, + port_id=db_data['id']) + if dns_data_db: + is_dns_name_changed = (dns_name is not None and + dns_data_db['current_dns_name'] != dns_name) + + if is_dns_name_changed or (has_fixed_ips and + dns_data_db['current_dns_name']): + dns_data_db['previous_dns_name'] = ( + dns_data_db['current_dns_name']) + dns_data_db['previous_dns_domain'] = ( + dns_data_db['current_dns_domain']) + if is_dns_name_changed: + dns_data_db[dns.DNSNAME] = dns_name + dns_data_db['current_dns_name'] = dns_name + if dns_name: + dns_data_db['current_dns_domain'] = dns_domain + else: + dns_data_db['current_dns_domain'] = '' + + dns_data_db.update() + return dns_data_db + if dns_name: + dns_data_db = port_obj.PortDNS(plugin_context, + port_id=db_data['id'], + current_dns_name=dns_name, + current_dns_domain=dns_domain, + previous_dns_name='', + previous_dns_domain='', + dns_name=dns_name) + dns_data_db.create() + return dns_data_db + + def process_update_port(self, plugin_context, request_data, db_data): + dns_name = request_data.get(dns.DNSNAME) + has_fixed_ips = 'fixed_ips' in request_data + if dns_name is None and not has_fixed_ips: + return + if dns_name is not None: + dns_name, is_dns_domain_default = self._get_request_dns_name( + request_data) + if is_dns_domain_default: + self._extend_port_dict(plugin_context.session, db_data, + db_data, None) + return + network = self._get_network(plugin_context, db_data['network_id']) + dns_domain = network[dns.DNSDOMAIN] + dns_data_db = None + if not dns_domain or self.external_dns_not_needed(plugin_context, + network): + # No need to update external DNS service. Only process the port's + # dns_name attribute if necessary + if dns_name is not None: + dns_data_db = self._process_only_dns_name_update( + plugin_context, db_data, dns_name) + else: + dns_data_db = self._update_dns_db(dns_name, dns_domain, db_data, + plugin_context, has_fixed_ips) + self._extend_port_dict(plugin_context.session, db_data, db_data, + dns_data_db) + + def _process_only_dns_name_update(self, plugin_context, db_data, dns_name): + dns_data_db = port_obj.PortDNS.get_object( + plugin_context, + port_id=db_data['id']) + if dns_data_db: + dns_data_db['dns_name'] = dns_name + dns_data_db.update() + return dns_data_db + if dns_name: + dns_data_db = port_obj.PortDNS(plugin_context, + port_id=db_data['id'], + current_dns_name='', + current_dns_domain='', + previous_dns_name='', + previous_dns_domain='', + dns_name=dns_name) + dns_data_db.create() + return dns_data_db + + def external_dns_not_needed(self, context, network): + """Decide if ports in network need to be sent to the DNS service. + + :param context: plugin request context + :param network: network dictionary + :return True or False + """ + pass + + def extend_network_dict(self, session, db_data, response_data): + response_data[dns.DNSDOMAIN] = '' + if db_data.dns_domain: + response_data[dns.DNSDOMAIN] = db_data.dns_domain[dns.DNSDOMAIN] + return response_data + + def _get_dns_domain(self): + if not cfg.CONF.dns_domain: + return '' + if cfg.CONF.dns_domain.endswith('.'): + return cfg.CONF.dns_domain + return '%s.' % cfg.CONF.dns_domain + + def _get_request_dns_name(self, port): + dns_domain = self._get_dns_domain() + if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)): + return (port.get(dns.DNSNAME, ''), False) + return ('', True) + + def _get_request_dns_name_and_domain_name(self, dns_data_db): + dns_domain = self._get_dns_domain() + dns_name = '' + if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)): + if dns_data_db: + dns_name = dns_data_db.dns_name + return dns_name, dns_domain + + def _get_dns_names_for_port(self, ips, dns_data_db): + dns_assignment = [] + dns_name, dns_domain = self._get_request_dns_name_and_domain_name( + dns_data_db) + for ip in ips: + if dns_name: + hostname = dns_name + fqdn = dns_name + if not dns_name.endswith('.'): + fqdn = '%s.%s' % (dns_name, dns_domain) + else: + hostname = 'host-%s' % ip['ip_address'].replace( + '.', '-').replace(':', '-') + fqdn = hostname + if dns_domain: + fqdn = '%s.%s' % (hostname, dns_domain) + dns_assignment.append({'ip_address': ip['ip_address'], + 'hostname': hostname, + 'fqdn': fqdn}) + return dns_assignment + + def _get_dns_name_for_port_get(self, port, dns_data_db): + if port['fixed_ips']: + return self._get_dns_names_for_port(port['fixed_ips'], dns_data_db) + return [] + + def _extend_port_dict(self, session, db_data, response_data, dns_data_db): + if not dns_data_db: + response_data[dns.DNSNAME] = '' + else: + response_data[dns.DNSNAME] = dns_data_db[dns.DNSNAME] + response_data['dns_assignment'] = self._get_dns_name_for_port_get( + db_data, dns_data_db) + return response_data + + def extend_port_dict(self, session, db_data, response_data): + dns_data_db = db_data.dns + return self._extend_port_dict(session, db_data, response_data, + dns_data_db) + + def _get_network(self, context, network_id): + plugin = directory.get_plugin() + return plugin.get_network(context, network_id) + + +class DNSExtensionDriverNSXv(DNSExtensionDriver): + + def initialize(self): + LOG.info(_LI("DNSExtensionDriverNSXv initialization complete")) + + def external_dns_not_needed(self, context, network): + dns_driver = _get_dns_driver() + if not dns_driver: + return True + if network['router:external']: + return True + return False + + +class DNSExtensionDriverNSXv3(DNSExtensionDriver): + + def initialize(self): + LOG.info(_LI("DNSExtensionDriverNSXv3 initialization complete")) + + def _get_dns_domain(self): + if cfg.CONF.nsx_v3.dns_domain: + dns_domain = cfg.CONF.nsx_v3.dns_domain + elif cfg.CONF.dns_domain: + dns_domain = cfg.CONF.dns_domain + else: + return '' + if dns_domain.endswith('.'): + return dns_domain + return '%s.' % dns_domain + + def external_dns_not_needed(self, context, network): + dns_driver = _get_dns_driver() + if not dns_driver: + return True + if network['router:external']: + return True + return False + + +DNS_DRIVER = None + + +def _get_dns_driver(): + global DNS_DRIVER + if DNS_DRIVER: + return DNS_DRIVER + if not cfg.CONF.external_dns_driver: + return + try: + DNS_DRIVER = driver.ExternalDNSService.get_instance() + LOG.debug("External DNS driver loaded: %s", + cfg.CONF.external_dns_driver) + return DNS_DRIVER + except ImportError: + LOG.exception(_LE("ImportError exception occurred while loading " + "the external DNS service driver")) + raise dns.ExternalDNSDriverNotFound( + driver=cfg.CONF.external_dns_driver) diff --git a/vmware_nsx/tests/unit/extension_drivers/__init__.py b/vmware_nsx/tests/unit/extension_drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/extension_drivers/test_dns_integration.py b/vmware_nsx/tests/unit/extension_drivers/test_dns_integration.py new file mode 100644 index 0000000000..0f88662366 --- /dev/null +++ b/vmware_nsx/tests/unit/extension_drivers/test_dns_integration.py @@ -0,0 +1,103 @@ +# Copyright 2017 VMware, Inc. +# 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. + +from neutron_lib.plugins import directory +from oslo_config import cfg + +from neutron import context +from neutron.extensions import dns + +from vmware_nsx.extension_drivers import dns_integration +from vmware_nsx.tests.unit.nsx_v import test_plugin as test_v_plugin +from vmware_nsx.tests.unit.nsx_v3 import test_plugin as test_v3_plugin + + +NETWORK_DOMAIN_NAME = 'net-domain.com.' +NEW_NETWORK_DOMAIN_NAME = 'new-net-domain.com.' +PORT_DNS_NAME = 'port-dns-name' +NEW_PORT_DNS_NAME = 'new-port-dns-name' + + +class NsxDNSIntegrationTestCase(object): + _domain = 'domain.com.' + dns_integration.DNS_DRIVER = None + + def test_create_network_dns_domain(self): + with self.network(dns_domain=NETWORK_DOMAIN_NAME, + arg_list=(dns.DNSDOMAIN,)) as network: + self.assertEqual(NETWORK_DOMAIN_NAME, + network['network'][dns.DNSDOMAIN]) + + def test_update_network_dns_domain(self): + with self.network(dns_domain=NETWORK_DOMAIN_NAME, + arg_list=(dns.DNSDOMAIN,)) as network: + update_data = {'network': {dns.DNSDOMAIN: NEW_NETWORK_DOMAIN_NAME}} + updated_network = directory.get_plugin().update_network( + context.get_admin_context(), network['network']['id'], + update_data) + self.assertEqual(NEW_NETWORK_DOMAIN_NAME, + updated_network[dns.DNSDOMAIN]) + + def test_create_port_dns_name(self): + with self.port(dns_name=PORT_DNS_NAME, + arg_list=(dns.DNSNAME,)) as port: + port_data = port['port'] + dns_assignment = port_data[dns.DNSASSIGNMENT][0] + self.assertEqual(PORT_DNS_NAME, port_data[dns.DNSNAME]) + self.assertEqual(PORT_DNS_NAME, dns_assignment['hostname']) + self.assertEqual(port_data['fixed_ips'][0]['ip_address'], + dns_assignment['ip_address']) + self.assertEqual(PORT_DNS_NAME + '.' + self._domain, + dns_assignment['fqdn']) + + def test_update_port_dns_name_ip(self): + with self.subnet(cidr='10.0.0.0/24') as subnet: + fixed_ips = [{'subnet_id': subnet['subnet']['id'], + 'ip_address': '10.0.0.3'}] + with self.port(subnet=subnet, fixed_ips=fixed_ips, + dns_name=PORT_DNS_NAME, + arg_list=(dns.DNSNAME,)) as port: + update_data = {'port': { + dns.DNSNAME: NEW_PORT_DNS_NAME, + 'fixed_ips': [{'subnet_id': subnet['subnet']['id'], + 'ip_address': '10.0.0.4'}]}} + updated_port = directory.get_plugin().update_port( + context.get_admin_context(), port['port']['id'], + update_data) + dns_assignment = updated_port[dns.DNSASSIGNMENT][0] + self.assertEqual(NEW_PORT_DNS_NAME, updated_port[dns.DNSNAME]) + self.assertEqual(NEW_PORT_DNS_NAME, dns_assignment['hostname']) + self.assertEqual(updated_port['fixed_ips'][0]['ip_address'], + dns_assignment['ip_address']) + self.assertEqual(NEW_PORT_DNS_NAME + '.' + self._domain, + dns_assignment['fqdn']) + + +class NsxVDNSIntegrationTestCase(NsxDNSIntegrationTestCase, + test_v_plugin.NsxVPluginV2TestCase): + + def setUp(self): + cfg.CONF.set_override('nsx_extension_drivers', ['vmware_nsxv_dns']) + cfg.CONF.set_override('dns_domain', self._domain) + super(NsxVDNSIntegrationTestCase, self).setUp() + + +class NsxV3DNSIntegrationTestCase(NsxDNSIntegrationTestCase, + test_v3_plugin.NsxV3PluginTestCaseMixin): + + def setUp(self): + cfg.CONF.set_override('nsx_extension_drivers', ['vmware_nsxv3_dns']) + cfg.CONF.set_override('dns_domain', self._domain, 'nsx_v3') + super(NsxV3DNSIntegrationTestCase, self).setUp()