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):