# Copyright (c) 2018 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.definitions import dns from neutron_lib.api import validators from neutron_lib.api.validators import availability_zone as az_validator 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.objects import registry as obj_reg from neutron_lib.plugins import directory from oslo_config import cfg from oslo_log import log as logging from neutron.db.models import dns as dns_model from neutron.db import models_v2 from neutron.services.externaldns import driver from vmware_nsx.common import driver_api from vmware_nsx.plugins.nsx_p import availability_zones as nsxp_az from vmware_nsx.plugins.nsx_v3 import availability_zones as nsx_az 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.ALIAS @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: obj_reg.new_instance('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 = obj_reg.load_class('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: obj_reg.new_instance('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) 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']) 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 = obj_reg.new_instance( '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): dns_data_db = obj_reg.load_class('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 = obj_reg.new_instance( '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, db_data['network_id'], plugin_context) if is_dns_domain_default: self._extend_port_dict(db_data, db_data, None, plugin_context) 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(db_data, db_data, dns_data_db, plugin_context) def _process_only_dns_name_update(self, plugin_context, db_data, dns_name): dns_data_db = obj_reg.load_class('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 = obj_reg.new_instance( '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, network_id, context=None): if not cfg.CONF.dns_domain: return '' 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) 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, network_id, context): dns_domain = self._get_dns_domain(network_id, context) 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, network_id, context): dns_assignment = [] dns_name, dns_domain = self._get_request_dns_name_and_domain_name( dns_data_db, network_id, context) 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, context): if port['fixed_ips']: return self._get_dns_names_for_port( port['fixed_ips'], dns_data_db, port['network_id'], context) return [] def _extend_port_dict(self, db_data, response_data, dns_data_db, context=None): 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, context) return response_data def extend_port_dict(self, session, db_data, response_data): dns_data_db = db_data.dns return self._extend_port_dict(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("DNSExtensionDriverNSXv initialization complete") 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 class DNSExtensionDriverNSXv3(DNSExtensionDriver): def initialize(self): self._availability_zones = nsx_az.NsxV3AvailabilityZones() LOG.info("DNSExtensionDriverNSXv3 initialization complete") self.config_dns_domain = cfg.CONF.nsx_v3.dns_domain def _get_db_net_dns(self, session, network_id): db_entry = session.query(dns_model.NetworkDNSDomain).filter_by( network_id=network_id).first() if db_entry: return db_entry.dns_domain def _get_db_net_az_hints(self, session, network_id): # TODO(asarfaty): Consider caching networks azs in get_ports # and use it here db_entry = session.query(models_v2.Network).filter_by( id=network_id).first() if db_entry: return db_entry.availability_zone_hints def _get_network_and_az_dns_domain(self, network_id, context): if not context: context = n_context.get_admin_context() # Getting only the relevant network attributes directly from the DB net_domain = self._get_db_net_dns(context.session, network_id) # Getting the az of the network is relevant only if any of the azs # have dns_domain and if there is no net_domain az_domain = None if not net_domain and self._availability_zones.non_default_dns_domain: az = None net_hints = self._get_db_net_az_hints(context.session, network_id) if net_hints: hints = az_validator.convert_az_string_to_list(net_hints) if hints: az_name = hints[0] az = self._availability_zones.get_availability_zone( az_name) if not az: # Get the default availability zone az = self._availability_zones.get_default_availability_zone() az_domain = az.dns_domain return net_domain, az_domain def _get_dns_domain(self, network_id, context=None): net_domain, az_domain = self._get_network_and_az_dns_domain( network_id, context) # first try to use the dns_domain configured on the network if net_domain: return _dotted_domain(net_domain) # try to use the dns-domain from the specific availability zone # of this network if (az_domain and _dotted_domain(az_domain) != _dotted_domain(DNS_DOMAIN_DEFAULT)): dns_domain = az_domain # Global nsx_v3 dns domain elif (self.config_dns_domain and (_dotted_domain(self.config_dns_domain) != _dotted_domain(DNS_DOMAIN_DEFAULT))): dns_domain = self.config_dns_domain # Global neutron dns domain elif cfg.CONF.dns_domain: dns_domain = cfg.CONF.dns_domain else: return '' 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 class DNSExtensionDriverNSXp(DNSExtensionDriverNSXv3): def initialize(self): self._availability_zones = nsxp_az.NsxPAvailabilityZones() LOG.info("DNSExtensionDriverNSXp initialization complete") self.config_dns_domain = cfg.CONF.nsx_p.dns_domain class DNSExtensionDriverDVS(DNSExtensionDriver): def initialize(self): LOG.info("DNSExtensionDriverDVS 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 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("ImportError exception occurred while loading " "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 = obj_reg.load_class('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 = obj_reg.load_class('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, payload=None): dns_driver = _get_dns_driver() if not dns_driver: return context = payload.context port_id = payload.resource_id dns_data_db = obj_reg.load_class('PortDNS').get_object( context, port_id=port_id) if not dns_data_db: return if dns_data_db['current_dns_name']: ip_allocations = obj_reg.load_class('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)