From 89b9171c58ef1dc06ca75d864060c6a5c1054577 Mon Sep 17 00:00:00 2001 From: Kobi Samoray <ksamoray@vmware.com> Date: Tue, 16 Jan 2018 12:30:43 +0200 Subject: [PATCH] NSXv3 DNS integration Complete NSXv3 integration with openstack/designate Change-Id: Ie91864e132ded03777013c88bf9dab7a09bc1289 --- .../extension_drivers/dns_integration.py | 198 +++++++++++++++--- vmware_nsx/plugins/nsx_v3/plugin.py | 11 + 2 files changed, 184 insertions(+), 25 deletions(-) diff --git a/vmware_nsx/extension_drivers/dns_integration.py b/vmware_nsx/extension_drivers/dns_integration.py index 84b6e48355..74e136b50e 100644 --- a/vmware_nsx/extension_drivers/dns_integration.py +++ b/vmware_nsx/extension_drivers/dns_integration.py @@ -1,5 +1,4 @@ -# Copyright (c) 2016 IBM -# Copyright (c) 2017 VMware, Inc. +# Copyright (c) 2018 VMware, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,6 +16,9 @@ from neutron_lib.api.definitions import availability_zone as az_def from neutron_lib.api.definitions import dns from neutron_lib.api import validators +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources from neutron_lib import context as n_context from neutron_lib.exceptions import dns as dns_exc from neutron_lib.plugins import directory @@ -34,6 +36,12 @@ LOG = logging.getLogger(__name__) DNS_DOMAIN_DEFAULT = 'openstacklocal.' +def _dotted_domain(dns_domain): + if dns_domain.endswith('.'): + return dns_domain + return '%s.' % dns_domain + + # TODO(asarfaty) use dns-domain/nameserver from network az instead of global class DNSExtensionDriver(driver_api.ExtensionDriver): _supported_extension_alias = 'dns-integration' @@ -81,27 +89,55 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): db_data[dns.DNSDOMAIN] = new_value def process_create_port(self, plugin_context, request_data, db_data): - if not request_data.get(dns.DNSNAME): + if not (request_data.get(dns.DNSNAME) or + request_data.get(dns.DNSDOMAIN)): return dns_name, is_dns_domain_default = self._get_request_dns_name( request_data, db_data['network_id'], plugin_context) 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() + self._create_port_dns_record(plugin_context, request_data, db_data, + network, dns_name) + + def _create_port_dns_record(self, plugin_context, request_data, db_data, + network, dns_name): + external_dns_domain = (request_data.get(dns.DNSDOMAIN) or + network.get(dns.DNSDOMAIN)) + current_dns_name, current_dns_domain = ( + self._calculate_current_dns_name_and_domain( + dns_name, external_dns_domain, + self.external_dns_not_needed(plugin_context, network))) + + dns_data_obj = 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, + dns_domain=request_data.get(dns.DNSDOMAIN, '')) + dns_data_obj.create() + return dns_data_obj + + def _calculate_current_dns_name_and_domain(self, dns_name, + external_dns_domain, + no_external_dns_service): + # When creating a new PortDNS object, the current_dns_name and + # current_dns_domain fields hold the data that the integration driver + # will send to the external DNS service. They are set to non-blank + # values only if all the following conditions are met: + # 1) There is an external DNS integration driver configured + # 2) The user request contains a valid non-blank value for the port's + # dns_name + # 3) The user request contains a valid non-blank value for the port's + # dns_domain or the port's network has a non-blank value in its + # dns_domain attribute + 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 def _update_dns_db(self, dns_name, dns_domain, db_data, plugin_context, has_fixed_ips): @@ -202,9 +238,7 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): def _get_dns_domain(self, network_id, context=None): if not cfg.CONF.dns_domain: return '' - if cfg.CONF.dns_domain.endswith('.'): - return cfg.CONF.dns_domain - return '%s.' % cfg.CONF.dns_domain + return _dotted_domain(cfg.CONF.dns_domain) def _get_request_dns_name(self, port, network_id, context): dns_domain = self._get_dns_domain(network_id, context) @@ -301,22 +335,28 @@ class DNSExtensionDriverNSXv3(DNSExtensionDriver): # try to get the dns-domain from the specific availability zone # of this network az = self._get_network_az(network_id, context) - if az.dns_domain: + if (az.dns_domain + and _dotted_domain(az.dns_domain) != + _dotted_domain(DNS_DOMAIN_DEFAULT)): dns_domain = az.dns_domain - elif cfg.CONF.nsx_v3.dns_domain: + elif (cfg.CONF.nsx_v3.dns_domain + and (_dotted_domain(cfg.CONF.nsx_v3.dns_domain) != + _dotted_domain(DNS_DOMAIN_DEFAULT))): 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 + return _dotted_domain(dns_domain) def external_dns_not_needed(self, context, network): dns_driver = _get_dns_driver() if not dns_driver: return True + provider_type = network.get('provider:network_type') + if not provider_type: + return True + if network['router:external']: return True return False @@ -355,3 +395,111 @@ def _get_dns_driver(): "the external DNS service driver") raise dns_exc.ExternalDNSDriverNotFound( driver=cfg.CONF.external_dns_driver) + + +def _send_data_to_external_dns_service(context, dns_driver, dns_domain, + dns_name, records): + try: + dns_driver.create_record_set(context, dns_domain, dns_name, records) + except (dns_exc.DNSDomainNotFound, dns_exc.DuplicateRecordSet) as e: + LOG.exception("Error publishing port data in external DNS " + "service. Name: '%(name)s'. Domain: '%(domain)s'. " + "DNS service driver message '%(message)s'", + {"name": dns_name, + "domain": dns_domain, + "message": e.msg}) + + +def _remove_data_from_external_dns_service(context, dns_driver, dns_domain, + dns_name, records): + try: + dns_driver.delete_record_set(context, dns_domain, dns_name, records) + except (dns_exc.DNSDomainNotFound, dns_exc.DuplicateRecordSet) as e: + LOG.exception("Error deleting port data from external DNS " + "service. Name: '%(name)s'. Domain: '%(domain)s'. " + "IP addresses '%(ips)s'. DNS service driver message " + "'%(message)s'", + {"name": dns_name, + "domain": dns_domain, + "message": e.msg, + "ips": ', '.join(records)}) + + +def _create_port_in_external_dns_service(resource, event, trigger, **kwargs): + dns_driver = _get_dns_driver() + if not dns_driver: + return + context = kwargs['context'] + port = kwargs['port'] + dns_data_db = port_obj.PortDNS.get_object( + context, port_id=port['id']) + if not (dns_data_db and dns_data_db['current_dns_name']): + return + records = [ip['ip_address'] for ip in port['fixed_ips']] + _send_data_to_external_dns_service(context, dns_driver, + dns_data_db['current_dns_domain'], + dns_data_db['current_dns_name'], + records) + + +def _update_port_in_external_dns_service(resource, event, trigger, **kwargs): + dns_driver = _get_dns_driver() + if not dns_driver: + return + context = kwargs['context'] + updated_port = kwargs['port'] + original_port = kwargs.get('original_port') + if not original_port: + return + original_ips = [ip['ip_address'] for ip in original_port['fixed_ips']] + updated_ips = [ip['ip_address'] for ip in updated_port['fixed_ips']] + is_dns_name_changed = (updated_port[dns.DNSNAME] != + original_port[dns.DNSNAME]) + is_dns_domain_changed = (dns.DNSDOMAIN in updated_port and + updated_port[dns.DNSDOMAIN] != + original_port[dns.DNSDOMAIN]) + ips_changed = set(original_ips) != set(updated_ips) + if not any((is_dns_name_changed, is_dns_domain_changed, ips_changed)): + return + dns_data_db = port_obj.PortDNS.get_object( + context, port_id=updated_port['id']) + if not (dns_data_db and + (dns_data_db['previous_dns_name'] or + dns_data_db['current_dns_name'])): + return + if dns_data_db['previous_dns_name']: + _remove_data_from_external_dns_service( + context, dns_driver, dns_data_db['previous_dns_domain'], + dns_data_db['previous_dns_name'], original_ips) + if dns_data_db['current_dns_name']: + _send_data_to_external_dns_service(context, dns_driver, + dns_data_db['current_dns_domain'], + dns_data_db['current_dns_name'], + updated_ips) + + +def _delete_port_in_external_dns_service(resource, event, trigger, **kwargs): + dns_driver = _get_dns_driver() + if not dns_driver: + return + context = kwargs['context'] + port_id = kwargs['port_id'] + dns_data_db = port_obj.PortDNS.get_object( + context, port_id=port_id) + if not dns_data_db: + return + if dns_data_db['current_dns_name']: + ip_allocations = port_obj.IPAllocation.get_objects(context, + port_id=port_id) + records = [str(alloc['ip_address']) for alloc in ip_allocations] + _remove_data_from_external_dns_service( + context, dns_driver, dns_data_db['current_dns_domain'], + dns_data_db['current_dns_name'], records) + + +registry.subscribe( + _create_port_in_external_dns_service, resources.PORT, events.AFTER_CREATE) +registry.subscribe( + _update_port_in_external_dns_service, resources.PORT, events.AFTER_UPDATE) +registry.subscribe( + _delete_port_in_external_dns_service, resources.PORT, events.BEFORE_DELETE) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 40ee815a16..b8a985af89 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -2612,6 +2612,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, if not cfg.CONF.nsx_v3.native_dhcp_metadata: nsx_rpc.handle_port_metadata_access(self, context, neutron_db) + kwargs = {'context': context, 'port': neutron_db} + registry.notify(resources.PORT, events.AFTER_CREATE, self, **kwargs) return port_data def _pre_delete_port_check(self, context, port_id, l2gw_port_check): @@ -3058,6 +3060,15 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, if cfg.CONF.nsx_v3.native_dhcp_metadata: self._update_dhcp_binding(context, original_port, updated_port) + # Notifications must be sent after the above transaction is complete + kwargs = { + 'context': context, + 'port': updated_port, + 'mac_address_updated': False, + 'original_port': original_port, + } + + registry.notify(resources.PORT, events.AFTER_UPDATE, self, **kwargs) return updated_port def _extend_get_port_dict_qos_and_binding(self, context, port):