External DNS driver reference implementation
An interface with an external DNS service is defined for Neutron. A reference implementation is also included, based on Designate. The interface and the driver will enable users to publish in the external DNS service the dns_name and dns_domain attributes associated with floating ips, ports and networks. As a consequence, the floating ips and networks api is extended to manage dns_name and dns_domain attributes. The dns_name attribute was added to ports in a preceding commit DocImpact: Introduce config option external_dns_driver to specify a driver for external dns integration. For more info, see doc/source/devref/external_dns_integration.rst APIImpact Implements: blueprint external-dns-resolution Change-Id: Ic298ad2558410ab9a614f22e1757d1fc8b22c482
This commit is contained in:
parent
424e6e4711
commit
93ac8b3a33
43
doc/source/devref/external_dns_integration.rst
Normal file
43
doc/source/devref/external_dns_integration.rst
Normal file
@ -0,0 +1,43 @@
|
||||
..
|
||||
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.
|
||||
|
||||
|
||||
Integration with external DNS services
|
||||
======================================
|
||||
|
||||
Since the Mitaka release, neutron has an interface defined to interact with an
|
||||
external DNS service. This interface is based on an abstract driver that can be
|
||||
used as the base class to implement concrete drivers to interact with various
|
||||
DNS services. The reference implementation of such a driver integrates neutron
|
||||
with
|
||||
`OpenStack Designate <http://docs.openstack.org/developer/designate/index.html>`_.
|
||||
|
||||
This integration allows users to publish *dns_name* and *dns_domain*
|
||||
attributes associated with floating IP addresses, ports, and networks in an
|
||||
external DNS service.
|
||||
|
||||
|
||||
Changes to the neutron API
|
||||
--------------------------
|
||||
|
||||
To support integration with an external DNS service, the *dns_name* and
|
||||
*dns_domain* attributes were added to floating ips, ports and networks. The
|
||||
*dns_name* specifies the name to be associated with a corresponding IP address,
|
||||
both of which will be published to an existing domain with the name
|
||||
*dns_domain* in the external DNS service.
|
||||
|
||||
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.
|
@ -127,6 +127,8 @@ core_opts = [
|
||||
cfg.StrOpt('dns_domain',
|
||||
default='openstacklocal',
|
||||
help=_('Domain to use for building the hostnames')),
|
||||
cfg.StrOpt('external_dns_driver',
|
||||
help=_('Driver for external DNS integration.')),
|
||||
cfg.BoolOpt('dhcp_agent_notification', default=True,
|
||||
help=_("Allow sending resource operation"
|
||||
" notification to DHCP agent")),
|
||||
|
328
neutron/db/dns_db.py
Normal file
328
neutron/db/dns_db.py
Normal file
@ -0,0 +1,328 @@
|
||||
# Copyright (c) 2016 IBM
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from neutron._i18n import _, _LE
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.common import exceptions as n_exc
|
||||
from neutron.common import utils
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.db import l3_db
|
||||
from neutron.db import model_base
|
||||
from neutron.db import models_v2
|
||||
from neutron.extensions import dns
|
||||
from neutron.extensions import l3
|
||||
from neutron import manager
|
||||
from neutron.plugins.common import constants as service_constants
|
||||
from neutron.services.externaldns import driver
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkDNSDomain(model_base.BASEV2):
|
||||
network_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('networks.id', ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True)
|
||||
dns_domain = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
|
||||
# Add a relationship to the Network model in order to instruct
|
||||
# SQLAlchemy to eagerly load this association
|
||||
network = orm.relationship(models_v2.Network,
|
||||
backref=orm.backref("dns_domain",
|
||||
lazy='joined',
|
||||
uselist=False,
|
||||
cascade='delete'))
|
||||
|
||||
|
||||
class FloatingIPDNS(model_base.BASEV2):
|
||||
|
||||
__tablename__ = 'floatingipdnses'
|
||||
|
||||
floatingip_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('floatingips.id',
|
||||
ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True)
|
||||
dns_name = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
dns_domain = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
published_dns_name = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
published_dns_domain = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
|
||||
# Add a relationship to the FloatingIP model in order to instruct
|
||||
# SQLAlchemy to eagerly load this association
|
||||
floatingip = orm.relationship(l3_db.FloatingIP,
|
||||
backref=orm.backref("dns",
|
||||
lazy='joined',
|
||||
uselist=False,
|
||||
cascade='delete'))
|
||||
|
||||
|
||||
class PortDNS(model_base.BASEV2):
|
||||
|
||||
__tablename__ = 'portdnses'
|
||||
|
||||
port_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('ports.id',
|
||||
ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True)
|
||||
current_dns_name = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
current_dns_domain = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
previous_dns_name = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
previous_dns_domain = sa.Column(sa.String(255),
|
||||
nullable=False)
|
||||
|
||||
# Add a relationship to the Port model in order to instruct
|
||||
# SQLAlchemy to eagerly load this association
|
||||
port = orm.relationship(models_v2.Port,
|
||||
backref=orm.backref("dns",
|
||||
lazy='joined',
|
||||
uselist=False,
|
||||
cascade='delete'))
|
||||
|
||||
|
||||
class DNSActionsData(object):
|
||||
|
||||
def __init__(self, current_dns_name=None, current_dns_domain=None,
|
||||
previous_dns_name=None, previous_dns_domain=None):
|
||||
self.current_dns_name = current_dns_name
|
||||
self.current_dns_domain = current_dns_domain
|
||||
self.previous_dns_name = previous_dns_name
|
||||
self.previous_dns_domain = previous_dns_domain
|
||||
|
||||
|
||||
class DNSDbMixin(object):
|
||||
"""Mixin class to add DNS methods to db_base_plugin_v2."""
|
||||
|
||||
_dns_driver = None
|
||||
|
||||
@property
|
||||
def dns_driver(self):
|
||||
if self._dns_driver:
|
||||
return self._dns_driver
|
||||
if not cfg.CONF.external_dns_driver:
|
||||
return
|
||||
try:
|
||||
self._dns_driver = driver.ExternalDNSService.get_instance()
|
||||
LOG.debug("External DNS driver loaded: %s",
|
||||
cfg.CONF.external_dns_driver)
|
||||
return self._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)
|
||||
|
||||
def _extend_floatingip_dict_dns(self, floatingip_res, floatingip_db):
|
||||
floatingip_res['dns_domain'] = ''
|
||||
floatingip_res['dns_name'] = ''
|
||||
if floatingip_db.dns:
|
||||
floatingip_res['dns_domain'] = floatingip_db.dns['dns_domain']
|
||||
floatingip_res['dns_name'] = floatingip_db.dns['dns_name']
|
||||
return floatingip_res
|
||||
|
||||
# Register dict extend functions for floating ips
|
||||
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
||||
l3.FLOATINGIPS, ['_extend_floatingip_dict_dns'])
|
||||
|
||||
def _process_dns_floatingip_create_precommit(self, context,
|
||||
floatingip_data, req_data):
|
||||
# expects to be called within a plugin's session
|
||||
dns_domain = req_data.get(dns.DNSDOMAIN)
|
||||
if not attributes.is_attr_set(dns_domain):
|
||||
return
|
||||
if not self.dns_driver:
|
||||
return
|
||||
|
||||
dns_name = req_data[dns.DNSNAME]
|
||||
self._validate_floatingip_dns(dns_name, dns_domain)
|
||||
|
||||
current_dns_name, current_dns_domain = (
|
||||
self._get_requested_state_for_external_dns_service_create(
|
||||
context, floatingip_data, req_data))
|
||||
dns_actions_data = None
|
||||
if current_dns_name and current_dns_domain:
|
||||
context.session.add(FloatingIPDNS(
|
||||
floatingip_id=floatingip_data['id'],
|
||||
dns_name=req_data[dns.DNSNAME],
|
||||
dns_domain=req_data[dns.DNSDOMAIN],
|
||||
published_dns_name=current_dns_name,
|
||||
published_dns_domain=current_dns_domain))
|
||||
dns_actions_data = DNSActionsData(
|
||||
current_dns_name=current_dns_name,
|
||||
current_dns_domain=current_dns_domain)
|
||||
floatingip_data['dns_name'] = dns_name
|
||||
floatingip_data['dns_domain'] = dns_domain
|
||||
return dns_actions_data
|
||||
|
||||
def _process_dns_floatingip_create_postcommit(self, context,
|
||||
floatingip_data,
|
||||
dns_actions_data):
|
||||
if not dns_actions_data:
|
||||
return
|
||||
self._add_ips_to_external_dns_service(
|
||||
context, dns_actions_data.current_dns_domain,
|
||||
dns_actions_data.current_dns_name,
|
||||
[floatingip_data['floating_ip_address']])
|
||||
|
||||
def _process_dns_floatingip_update_precommit(self, context,
|
||||
floatingip_data):
|
||||
# expects to be called within a plugin's session
|
||||
plugin = manager.NeutronManager.get_service_plugins().get(
|
||||
service_constants.L3_ROUTER_NAT)
|
||||
if not utils.is_extension_supported(plugin, dns.Dns.get_alias()):
|
||||
return
|
||||
if not self.dns_driver:
|
||||
return
|
||||
dns_data_db = context.session.query(FloatingIPDNS).filter_by(
|
||||
floatingip_id=floatingip_data['id']).one_or_none()
|
||||
if dns_data_db and dns_data_db['dns_name']:
|
||||
# dns_name and dns_domain assigned for floating ip. It doesn't
|
||||
# matter whether they are defined for internal port
|
||||
return
|
||||
current_dns_name, current_dns_domain = (
|
||||
self._get_requested_state_for_external_dns_service_update(
|
||||
context, floatingip_data))
|
||||
if dns_data_db:
|
||||
if (dns_data_db['published_dns_name'] != current_dns_name or
|
||||
dns_data_db['published_dns_domain'] != current_dns_domain):
|
||||
dns_actions_data = DNSActionsData(
|
||||
previous_dns_name=dns_data_db['published_dns_name'],
|
||||
previous_dns_domain=dns_data_db['published_dns_domain'])
|
||||
if current_dns_name and current_dns_domain:
|
||||
dns_data_db['published_dns_name'] = current_dns_name
|
||||
dns_data_db['published_dns_domain'] = current_dns_domain
|
||||
dns_actions_data.current_dns_name = current_dns_name
|
||||
dns_actions_data.current_dns_domain = current_dns_domain
|
||||
else:
|
||||
context.session.delete(dns_data_db)
|
||||
return dns_actions_data
|
||||
else:
|
||||
return
|
||||
if current_dns_name and current_dns_domain:
|
||||
context.session.add(FloatingIPDNS(
|
||||
floatingip_id=floatingip_data['id'],
|
||||
dns_name='',
|
||||
dns_domain='',
|
||||
published_dns_name=current_dns_name,
|
||||
published_dns_domain=current_dns_domain))
|
||||
return DNSActionsData(current_dns_name=current_dns_name,
|
||||
current_dns_domain=current_dns_domain)
|
||||
|
||||
def _process_dns_floatingip_update_postcommit(self, context,
|
||||
floatingip_data,
|
||||
dns_actions_data):
|
||||
if not dns_actions_data:
|
||||
return
|
||||
if dns_actions_data.previous_dns_name:
|
||||
self._delete_floatingip_from_external_dns_service(
|
||||
context, dns_actions_data.previous_dns_domain,
|
||||
dns_actions_data.previous_dns_name,
|
||||
[floatingip_data['floating_ip_address']])
|
||||
if dns_actions_data.current_dns_name:
|
||||
self._add_ips_to_external_dns_service(
|
||||
context, dns_actions_data.current_dns_domain,
|
||||
dns_actions_data.current_dns_name,
|
||||
[floatingip_data['floating_ip_address']])
|
||||
|
||||
def _process_dns_floatingip_delete(self, context, floatingip_data):
|
||||
plugin = manager.NeutronManager.get_service_plugins().get(
|
||||
service_constants.L3_ROUTER_NAT)
|
||||
if not utils.is_extension_supported(plugin, dns.Dns.get_alias()):
|
||||
return
|
||||
dns_data_db = context.session.query(FloatingIPDNS).filter_by(
|
||||
floatingip_id=floatingip_data['id']).one_or_none()
|
||||
if dns_data_db:
|
||||
self._delete_floatingip_from_external_dns_service(
|
||||
context, dns_data_db['published_dns_domain'],
|
||||
dns_data_db['published_dns_name'],
|
||||
[floatingip_data['floating_ip_address']])
|
||||
|
||||
def _validate_floatingip_dns(self, dns_name, dns_domain):
|
||||
if dns_domain and not dns_name:
|
||||
msg = _("dns_domain cannot be specified without a dns_name")
|
||||
raise n_exc.BadRequest(resource='floatingip', msg=msg)
|
||||
if dns_name and not dns_domain:
|
||||
msg = _("dns_name cannot be specified without a dns_domain")
|
||||
raise n_exc.BadRequest(resource='floatingip', msg=msg)
|
||||
|
||||
def _get_internal_port_dns_data(self, context, floatingip_data):
|
||||
internal_port = context.session.query(models_v2.Port).filter_by(
|
||||
id=floatingip_data['port_id']).one()
|
||||
dns_domain = None
|
||||
if internal_port['dns_name']:
|
||||
net_dns = context.session.query(NetworkDNSDomain).filter_by(
|
||||
network_id=internal_port['network_id']).one_or_none()
|
||||
if net_dns:
|
||||
dns_domain = net_dns['dns_domain']
|
||||
return internal_port['dns_name'], dns_domain
|
||||
|
||||
def _delete_floatingip_from_external_dns_service(self, context, dns_domain,
|
||||
dns_name, records):
|
||||
try:
|
||||
self.dns_driver.delete_record_set(context, dns_domain, dns_name,
|
||||
records)
|
||||
except (dns.DNSDomainNotFound, dns.DuplicateRecordSet) as e:
|
||||
LOG.exception(_LE("Error deleting Floating IP 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 _get_requested_state_for_external_dns_service_create(self, context,
|
||||
floatingip_data,
|
||||
req_data):
|
||||
fip_dns_name = req_data[dns.DNSNAME]
|
||||
if fip_dns_name:
|
||||
return fip_dns_name, req_data[dns.DNSDOMAIN]
|
||||
if floatingip_data['port_id']:
|
||||
return self._get_internal_port_dns_data(context, floatingip_data)
|
||||
return None, None
|
||||
|
||||
def _get_requested_state_for_external_dns_service_update(self, context,
|
||||
floatingip_data):
|
||||
if floatingip_data['port_id']:
|
||||
return self._get_internal_port_dns_data(context, floatingip_data)
|
||||
return None, None
|
||||
|
||||
def _add_ips_to_external_dns_service(self, context, dns_domain, dns_name,
|
||||
records):
|
||||
try:
|
||||
self.dns_driver.create_record_set(context, dns_domain, dns_name,
|
||||
records)
|
||||
except (dns.DNSDomainNotFound, dns.DuplicateRecordSet) as e:
|
||||
LOG.exception(_LE("Error publishing floating IP 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})
|
@ -783,7 +783,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
raise l3.FloatingIPNotFound(floatingip_id=id)
|
||||
return floatingip
|
||||
|
||||
def _make_floatingip_dict(self, floatingip, fields=None):
|
||||
def _make_floatingip_dict(self, floatingip, fields=None,
|
||||
process_extensions=True):
|
||||
res = {'id': floatingip['id'],
|
||||
'tenant_id': floatingip['tenant_id'],
|
||||
'floating_ip_address': floatingip['floating_ip_address'],
|
||||
@ -792,6 +793,11 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
'port_id': floatingip['fixed_port_id'],
|
||||
'fixed_ip_address': floatingip['fixed_ip_address'],
|
||||
'status': floatingip['status']}
|
||||
# NOTE(mlavalle): The following assumes this mixin is used in a
|
||||
# class inheriting from CommonDbMixin, which is true for all existing
|
||||
# plugins.
|
||||
if process_extensions:
|
||||
self._apply_dict_extend_functions(l3.FLOATINGIPS, res, floatingip)
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _get_router_for_floatingip(self, context, internal_port,
|
||||
@ -1012,8 +1018,15 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
self._update_fip_assoc(context, fip,
|
||||
floatingip_db, external_port)
|
||||
context.session.add(floatingip_db)
|
||||
floatingip_dict = self._make_floatingip_dict(
|
||||
floatingip_db, process_extensions=False)
|
||||
dns_actions_data = self._process_dns_floatingip_create_precommit(
|
||||
context, floatingip_dict, fip)
|
||||
|
||||
return self._make_floatingip_dict(floatingip_db)
|
||||
self._process_dns_floatingip_create_postcommit(context,
|
||||
floatingip_dict,
|
||||
dns_actions_data)
|
||||
return floatingip_dict
|
||||
|
||||
def create_floatingip(self, context, floatingip,
|
||||
initial_status=l3_constants.FLOATINGIP_STATUS_ACTIVE):
|
||||
@ -1030,7 +1043,13 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
self._update_fip_assoc(context, fip, floatingip_db,
|
||||
self._core_plugin.get_port(
|
||||
context.elevated(), fip_port_id))
|
||||
return old_floatingip, self._make_floatingip_dict(floatingip_db)
|
||||
floatingip_dict = self._make_floatingip_dict(floatingip_db)
|
||||
dns_actions_data = self._process_dns_floatingip_update_precommit(
|
||||
context, floatingip_dict)
|
||||
self._process_dns_floatingip_update_postcommit(context,
|
||||
floatingip_dict,
|
||||
dns_actions_data)
|
||||
return old_floatingip, floatingip_dict
|
||||
|
||||
def _floatingips_to_router_ids(self, floatingips):
|
||||
return list(set([floatingip['router_id']
|
||||
@ -1050,6 +1069,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
|
||||
def _delete_floatingip(self, context, id):
|
||||
floatingip = self._get_floatingip(context, id)
|
||||
floatingip_dict = self._make_floatingip_dict(floatingip)
|
||||
self._process_dns_floatingip_delete(context, floatingip_dict)
|
||||
# Foreign key cascade will take care of the removal of the
|
||||
# floating IP record once the port is deleted. We can't start
|
||||
# a transaction first to remove it ourselves because the delete_port
|
||||
@ -1057,7 +1078,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
|
||||
self._core_plugin.delete_port(context.elevated(),
|
||||
floatingip['floating_port_id'],
|
||||
l3_port_check=False)
|
||||
return self._make_floatingip_dict(floatingip)
|
||||
return floatingip_dict
|
||||
|
||||
def delete_floatingip(self, context, id):
|
||||
self._delete_floatingip(context, id)
|
||||
|
@ -1 +1 @@
|
||||
c3a73f615e4
|
||||
659bf3d90664
|
||||
|
@ -0,0 +1,87 @@
|
||||
# Copyright 2016 IBM
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Add tables and attributes to support external DNS integration
|
||||
|
||||
Revision ID: 659bf3d90664
|
||||
Revises: c3a73f615e4
|
||||
Create Date: 2015-09-11 00:22:47.618593
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '659bf3d90664'
|
||||
down_revision = 'c3a73f615e4'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from neutron.extensions import dns
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('networkdnsdomains',
|
||||
sa.Column('network_id',
|
||||
sa.String(length=36),
|
||||
nullable=False,
|
||||
index=True),
|
||||
sa.Column('dns_domain', sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.ForeignKeyConstraint(['network_id'],
|
||||
['networks.id'],
|
||||
ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('network_id'))
|
||||
|
||||
op.create_table('floatingipdnses',
|
||||
sa.Column('floatingip_id',
|
||||
sa.String(length=36),
|
||||
nullable=False,
|
||||
index=True),
|
||||
sa.Column('dns_name', sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('dns_domain', sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('published_dns_name',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('published_dns_domain',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.ForeignKeyConstraint(['floatingip_id'],
|
||||
['floatingips.id'],
|
||||
ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('floatingip_id'))
|
||||
|
||||
op.create_table('portdnses',
|
||||
sa.Column('port_id',
|
||||
sa.String(length=36),
|
||||
nullable=False,
|
||||
index=True),
|
||||
sa.Column('current_dns_name',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('current_dns_domain',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('previous_dns_name',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.Column('previous_dns_domain',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=False),
|
||||
sa.ForeignKeyConstraint(['port_id'],
|
||||
['ports.id'],
|
||||
ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('port_id'))
|
@ -22,6 +22,7 @@ from neutron._i18n import _
|
||||
from neutron.api import extensions
|
||||
from neutron.api.v2 import attributes as attr
|
||||
from neutron.common import exceptions as n_exc
|
||||
from neutron.extensions import l3
|
||||
|
||||
DNS_LABEL_MAX_LEN = 63
|
||||
DNS_LABEL_REGEX = "[a-z0-9-]{1,%d}$" % DNS_LABEL_MAX_LEN
|
||||
@ -29,6 +30,24 @@ FQDN_MAX_LEN = 255
|
||||
DNS_DOMAIN_DEFAULT = 'openstacklocal.'
|
||||
|
||||
|
||||
class DNSDomainNotFound(n_exc.NotFound):
|
||||
message = _("Domain %(dns_domain)s not found in the external DNS service")
|
||||
|
||||
|
||||
class DuplicateRecordSet(n_exc.Conflict):
|
||||
message = _("Name %(dns_name)s is duplicated in the external DNS service")
|
||||
|
||||
|
||||
class ExternalDNSDriverNotFound(n_exc.NotFound):
|
||||
message = _("External DNS driver %(driver)s could not be found.")
|
||||
|
||||
|
||||
class InvalidPTRZoneConfiguration(n_exc.Conflict):
|
||||
message = _("Value of %(parameter)s has to be multiple of %(number)s, "
|
||||
"with maximum value of %(maximum)s and minimum value of "
|
||||
"%(minimum)s")
|
||||
|
||||
|
||||
def _validate_dns_name(data, max_len=FQDN_MAX_LEN):
|
||||
msg = _validate_dns_format(data, max_len)
|
||||
if msg:
|
||||
@ -40,6 +59,50 @@ def _validate_dns_name(data, max_len=FQDN_MAX_LEN):
|
||||
return msg
|
||||
|
||||
|
||||
def _validate_fip_dns_name(data, max_len=FQDN_MAX_LEN):
|
||||
msg = attr._validate_string(data)
|
||||
if msg:
|
||||
return msg
|
||||
if not data:
|
||||
return
|
||||
if data.endswith('.'):
|
||||
msg = _("'%s' is a FQDN. It should be a relative domain name") % data
|
||||
return msg
|
||||
msg = _validate_dns_format(data, max_len)
|
||||
if msg:
|
||||
return msg
|
||||
length = len(data)
|
||||
if length > max_len - 3:
|
||||
msg = _("'%(data)s' contains '%(length)s' characters. Adding a "
|
||||
"domain name will cause it to exceed the maximum length "
|
||||
"of a FQDN of '%(max_len)s'") % {"data": data,
|
||||
"length": length,
|
||||
"max_len": max_len}
|
||||
return msg
|
||||
|
||||
|
||||
def _validate_dns_domain(data, max_len=FQDN_MAX_LEN):
|
||||
msg = attr._validate_string(data)
|
||||
if msg:
|
||||
return msg
|
||||
if not data:
|
||||
return
|
||||
if not data.endswith('.'):
|
||||
msg = _("'%s' is not a FQDN") % data
|
||||
return msg
|
||||
msg = _validate_dns_format(data, max_len)
|
||||
if msg:
|
||||
return msg
|
||||
length = len(data)
|
||||
if length > max_len - 2:
|
||||
msg = _("'%(data)s' contains '%(length)s' characters. Adding a "
|
||||
"sub-domain will cause it to exceed the maximum length of a "
|
||||
"FQDN of '%(max_len)s'") % {"data": data,
|
||||
"length": length,
|
||||
"max_len": max_len}
|
||||
return msg
|
||||
|
||||
|
||||
def _validate_dns_format(data, max_len=FQDN_MAX_LEN):
|
||||
# NOTE: An individual name regex instead of an entire FQDN was used
|
||||
# because its easier to make correct. The logic should validate that the
|
||||
@ -133,11 +196,13 @@ def convert_to_lowercase(data):
|
||||
raise n_exc.InvalidInput(error_message=msg)
|
||||
|
||||
|
||||
attr.validators['type:dns_name'] = (
|
||||
_validate_dns_name)
|
||||
attr.validators['type:dns_name'] = (_validate_dns_name)
|
||||
attr.validators['type:fip_dns_name'] = (_validate_fip_dns_name)
|
||||
attr.validators['type:dns_domain'] = (_validate_dns_domain)
|
||||
|
||||
|
||||
DNSNAME = 'dns_name'
|
||||
DNSDOMAIN = 'dns_domain'
|
||||
DNSASSIGNMENT = 'dns_assignment'
|
||||
EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'ports': {
|
||||
@ -148,7 +213,26 @@ EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'is_visible': True},
|
||||
DNSASSIGNMENT: {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
}
|
||||
},
|
||||
l3.FLOATINGIPS: {
|
||||
DNSNAME: {'allow_post': True, 'allow_put': False,
|
||||
'default': '',
|
||||
'convert_to': convert_to_lowercase,
|
||||
'validate': {'type:fip_dns_name': FQDN_MAX_LEN},
|
||||
'is_visible': True},
|
||||
DNSDOMAIN: {'allow_post': True, 'allow_put': False,
|
||||
'default': '',
|
||||
'convert_to': convert_to_lowercase,
|
||||
'validate': {'type:dns_domain': FQDN_MAX_LEN},
|
||||
'is_visible': True},
|
||||
},
|
||||
attr.NETWORKS: {
|
||||
DNSDOMAIN: {'allow_post': True, 'allow_put': True,
|
||||
'default': '',
|
||||
'convert_to': convert_to_lowercase,
|
||||
'validate': {'type:dns_domain': FQDN_MAX_LEN},
|
||||
'is_visible': True},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +249,7 @@ class Dns(extensions.ExtensionDescriptor):
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Provides integration with internal DNS."
|
||||
return "Provides integration with DNS."
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
|
@ -33,3 +33,8 @@ class ExtensionDriverNotFound(exceptions.InvalidConfigurationOption):
|
||||
"""Required extension driver not found in ML2 config."""
|
||||
message = _("Extension driver %(driver)s required for "
|
||||
"service plugin %(service_plugin)s not found.")
|
||||
|
||||
|
||||
class UnknownNetworkType(exceptions.NeutronException):
|
||||
"""Network with unknown type."""
|
||||
message = _("Unknown network type %(network_type)s.")
|
||||
|
328
neutron/plugins/ml2/extensions/dns_integration.py
Normal file
328
neutron/plugins/ml2/extensions/dns_integration.py
Normal file
@ -0,0 +1,328 @@
|
||||
# Copyright (c) 2016 IBM
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron._i18n import _LE, _LI
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.callbacks import events
|
||||
from neutron.callbacks import registry
|
||||
from neutron.callbacks import resources
|
||||
from neutron.db import dns_db
|
||||
from neutron.db import models_v2
|
||||
from neutron.extensions import dns
|
||||
from neutron import manager
|
||||
from neutron.plugins.common import utils as plugin_utils
|
||||
from neutron.plugins.ml2 import db
|
||||
from neutron.plugins.ml2 import driver_api as api
|
||||
from neutron.services.externaldns import driver
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DNSExtensionDriver(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 attributes.is_attr_set(dns_domain):
|
||||
return
|
||||
|
||||
if dns_domain:
|
||||
plugin_context.session.add(dns_db.NetworkDNSDomain(
|
||||
network_id=db_data['id'], dns_domain=dns_domain))
|
||||
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 attributes.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 = plugin_context.session.query(
|
||||
dns_db.NetworkDNSDomain).filter_by(network_id=net_id).one()
|
||||
if new_value:
|
||||
net_dns_domain['dns_domain'] = new_value
|
||||
db_data[dns.DNSDOMAIN] = new_value
|
||||
else:
|
||||
plugin_context.session.delete(net_dns_domain)
|
||||
db_data[dns.DNSDOMAIN] = ''
|
||||
elif new_value:
|
||||
plugin_context.session.add(dns_db.NetworkDNSDomain(
|
||||
network_id=net_id, dns_domain=new_value))
|
||||
db_data[dns.DNSDOMAIN] = new_value
|
||||
|
||||
def process_create_port(self, plugin_context, request_data, db_data):
|
||||
if not request_data[dns.DNSNAME]:
|
||||
return
|
||||
network = self._get_network(plugin_context, db_data['network_id'])
|
||||
if not network[dns.DNSDOMAIN]:
|
||||
return
|
||||
if self.external_dns_not_needed(plugin_context, network):
|
||||
return
|
||||
plugin_context.session.add(dns_db.PortDNS(
|
||||
port_id=db_data['id'],
|
||||
current_dns_name=request_data[dns.DNSNAME],
|
||||
current_dns_domain=network[dns.DNSDOMAIN],
|
||||
previous_dns_name='', previous_dns_domain=''))
|
||||
|
||||
def process_update_port(self, plugin_context, request_data, db_data):
|
||||
dns_name = request_data.get(dns.DNSNAME)
|
||||
if dns_name is None:
|
||||
return
|
||||
network = self._get_network(plugin_context, db_data['network_id'])
|
||||
if not network[dns.DNSDOMAIN]:
|
||||
return
|
||||
if self.external_dns_not_needed(plugin_context, network):
|
||||
return
|
||||
dns_domain = network[dns.DNSDOMAIN]
|
||||
dns_data_db = plugin_context.session.query(dns_db.PortDNS).filter_by(
|
||||
port_id=db_data['id']).one_or_none()
|
||||
if dns_data_db:
|
||||
if dns_name:
|
||||
if dns_data_db['current_dns_name'] != 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'])
|
||||
dns_data_db['current_dns_name'] = dns_name
|
||||
dns_data_db['current_dns_domain'] = dns_domain
|
||||
return
|
||||
if 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'])
|
||||
dns_data_db['current_dns_name'] = ''
|
||||
dns_data_db['current_dns_domain'] = ''
|
||||
return
|
||||
if dns_name:
|
||||
plugin_context.session.add(dns_db.PortDNS(
|
||||
port_id=db_data['id'],
|
||||
current_dns_name=dns_name,
|
||||
current_dns_domain=dns_domain,
|
||||
previous_dns_name='', previous_dns_domain=''))
|
||||
|
||||
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 extend_port_dict(self, session, db_data, response_data):
|
||||
response_data[dns.DNSNAME] = db_data[dns.DNSNAME]
|
||||
return response_data
|
||||
|
||||
def _get_network(self, context, network_id):
|
||||
plugin = manager.NeutronManager.get_plugin()
|
||||
return plugin.get_network(context, network_id)
|
||||
|
||||
|
||||
class DNSExtensionDriverML2(DNSExtensionDriver):
|
||||
|
||||
def initialize(self):
|
||||
LOG.info(_LI("DNSExtensionDriverML2 initialization complete"))
|
||||
|
||||
def _is_tunnel_tenant_network(self, provider_net):
|
||||
if provider_net['network_type'] == 'geneve':
|
||||
tunnel_ranges = cfg.CONF.ml2_type_geneve.vni_ranges
|
||||
elif provider_net['network_type'] == 'vxlan':
|
||||
tunnel_ranges = cfg.CONF.ml2_type_vxlan.vni_ranges
|
||||
else:
|
||||
tunnel_ranges = cfg.CONF.ml2_type_gre.tunnel_id_ranges
|
||||
|
||||
segmentation_id = int(provider_net['segmentation_id'])
|
||||
for entry in tunnel_ranges:
|
||||
entry = entry.strip()
|
||||
tun_min, tun_max = entry.split(':')
|
||||
tun_min = tun_min.strip()
|
||||
tun_max = tun_max.strip()
|
||||
return int(tun_min) <= segmentation_id <= int(tun_max)
|
||||
|
||||
def _is_vlan_tenant_network(self, provider_net):
|
||||
network_vlan_ranges = plugin_utils.parse_network_vlan_ranges(
|
||||
cfg.CONF.ml2_type_vlan.network_vlan_ranges)
|
||||
vlan_ranges = network_vlan_ranges[provider_net['physical_network']]
|
||||
if not vlan_ranges:
|
||||
return False
|
||||
segmentation_id = int(provider_net['segmentation_id'])
|
||||
for vlan_range in vlan_ranges:
|
||||
if vlan_range[0] <= segmentation_id <= vlan_range[1]:
|
||||
return True
|
||||
|
||||
def external_dns_not_needed(self, context, network):
|
||||
if not DNS_DRIVER:
|
||||
return True
|
||||
if network['router:external']:
|
||||
return True
|
||||
segments = db.get_network_segments(context.session, network['id'])
|
||||
if len(segments) > 1:
|
||||
return False
|
||||
provider_net = segments[0]
|
||||
if provider_net['network_type'] == 'local':
|
||||
return True
|
||||
if provider_net['network_type'] == 'flat':
|
||||
return False
|
||||
if provider_net['network_type'] == 'vlan':
|
||||
return self._is_vlan_tenant_network(provider_net)
|
||||
if provider_net['network_type'] in ['gre', 'vxlan', 'geneve']:
|
||||
return self._is_tunnel_tenant_network(provider_net)
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 = context.session.query(dns_db.PortDNS).filter_by(
|
||||
port_id=port['id']).one_or_none()
|
||||
if not dns_data_db:
|
||||
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 _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.DNSDomainNotFound, dns.DuplicateRecordSet) as e:
|
||||
LOG.exception(_LE("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.DNSDomainNotFound, dns.DuplicateRecordSet) as e:
|
||||
LOG.exception(_LE("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 _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
|
||||
if updated_port[dns.DNSNAME] == original_port[dns.DNSNAME]:
|
||||
return
|
||||
dns_data_db = context.session.query(dns_db.PortDNS).filter_by(
|
||||
port_id=updated_port['id']).one_or_none()
|
||||
if not dns_data_db:
|
||||
return
|
||||
if dns_data_db['previous_dns_name']:
|
||||
records = [ip['ip_address'] for ip in original_port['fixed_ips']]
|
||||
_remove_data_from_external_dns_service(
|
||||
context, dns_driver, dns_data_db['previous_dns_domain'],
|
||||
dns_data_db['previous_dns_name'], records)
|
||||
if dns_data_db['current_dns_name']:
|
||||
records = [ip['ip_address'] for ip in updated_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 _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 = context.session.query(dns_db.PortDNS).filter_by(
|
||||
port_id=port_id).one_or_none()
|
||||
if not dns_data_db:
|
||||
return
|
||||
if dns_data_db['current_dns_name']:
|
||||
ip_allocations = context.session.query(
|
||||
models_v2.IPAllocation).filter_by(port_id=port_id).all()
|
||||
records = [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)
|
||||
|
||||
|
||||
def subscribe():
|
||||
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)
|
||||
|
||||
subscribe()
|
0
neutron/services/externaldns/__init__.py
Normal file
0
neutron/services/externaldns/__init__.py
Normal file
74
neutron/services/externaldns/driver.py
Normal file
74
neutron/services/externaldns/driver.py
Normal file
@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2016 IBM
|
||||
# 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 abc
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from neutron import manager
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ExternalDNSService(object):
|
||||
"""Interface definition for an external dns service driver."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize external dns service driver."""
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Return an instance of the configured external DNS driver."""
|
||||
external_dns_driver_name = cfg.CONF.external_dns_driver
|
||||
mgr = manager.NeutronManager
|
||||
LOG.debug("Loading external dns driver: %s", external_dns_driver_name)
|
||||
driver_class = mgr.load_class_for_provider(
|
||||
'neutron.services.external_dns_drivers', external_dns_driver_name)
|
||||
return driver_class()
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_record_set(self, context, dns_domain, dns_name, records):
|
||||
"""Create a record set in the specified zone.
|
||||
|
||||
:param context: neutron api request context
|
||||
:type context: neutron.context.Context
|
||||
:param dns_domain: the dns_domain where the record set will be created
|
||||
:type dns_domain: String
|
||||
:param dns_name: the name associated with the record set
|
||||
:type dns_name: String
|
||||
:param records: the records in the set
|
||||
:type records: List of Strings
|
||||
:raises: neutron.extensions.dns.DNSDomainNotFound
|
||||
neutron.extensions.dns.DuplicateRecordSet
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_record_set(self, context, dns_domain, dns_name, records):
|
||||
"""Delete a record set in the specified zone.
|
||||
|
||||
:param context: neutron api request context
|
||||
:type context: neutron.context.Context
|
||||
:param dns_domain: the dns_domain from which the record set will be
|
||||
deleted
|
||||
:type dns_domain: String
|
||||
:param dns_name: the dns_name associated with the record set to be
|
||||
deleted
|
||||
:type dns_name: String
|
||||
:param records: the records in the set to be deleted
|
||||
:type records: List of Strings
|
||||
"""
|
0
neutron/services/externaldns/drivers/__init__.py
Normal file
0
neutron/services/externaldns/drivers/__init__.py
Normal file
206
neutron/services/externaldns/drivers/designate/driver.py
Normal file
206
neutron/services/externaldns/drivers/designate/driver.py
Normal file
@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2016 IBM
|
||||
# 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 designateclient import exceptions as d_exc
|
||||
from designateclient.v2 import client as d_client
|
||||
from keystoneclient.auth.identity.generic import password
|
||||
from keystoneclient.auth import token_endpoint
|
||||
from keystoneclient import session
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.extensions import dns
|
||||
from neutron.services.externaldns import driver
|
||||
|
||||
IPV4_PTR_ZONE_PREFIX_MIN_SIZE = 8
|
||||
IPV4_PTR_ZONE_PREFIX_MAX_SIZE = 24
|
||||
IPV6_PTR_ZONE_PREFIX_MIN_SIZE = 4
|
||||
IPV6_PTR_ZONE_PREFIX_MAX_SIZE = 124
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
_SESSION = None
|
||||
|
||||
designate_opts = [
|
||||
cfg.StrOpt('url',
|
||||
help=_('URL for connecting to designate')),
|
||||
cfg.StrOpt('admin_username',
|
||||
help=_('Username for connecting to designate in admin '
|
||||
'context')),
|
||||
cfg.StrOpt('admin_password',
|
||||
help=_('Password for connecting to designate in admin '
|
||||
'context'),
|
||||
secret=True),
|
||||
cfg.StrOpt('admin_tenant_id',
|
||||
help=_('Tenant id for connecting to designate in admin '
|
||||
'context')),
|
||||
cfg.StrOpt('admin_tenant_name',
|
||||
help=_('Tenant name for connecting to designate in admin '
|
||||
'context')),
|
||||
cfg.StrOpt('admin_auth_url',
|
||||
help=_('Authorization URL for connecting to designate in admin '
|
||||
'context')),
|
||||
cfg.BoolOpt('allow_reverse_dns_lookup', default=True,
|
||||
help=_('Allow the creation of PTR records')),
|
||||
cfg.IntOpt('ipv4_ptr_zone_prefix_size', default=24,
|
||||
help=_('Number of bits in an ipv4 PTR zone that will be considered '
|
||||
'network prefix. It has to align to byte boundary. Minimum '
|
||||
'value is 8. Maximum value is 24. As a consequence, range '
|
||||
'of values is 8, 16 and 24')),
|
||||
cfg.IntOpt('ipv6_ptr_zone_prefix_size', default=120,
|
||||
help=_('Number of bits in an ipv6 PTR zone that will be considered '
|
||||
'network prefix. It has to align to nyble boundary. Minimum '
|
||||
'value is 4. Maximum value is 124. As a consequence, range '
|
||||
'of values is 4, 8, 12, 16,..., 124')),
|
||||
cfg.StrOpt('ptr_zone_email', default='',
|
||||
help=_('The email address to be used when creating PTR zones. '
|
||||
'If not specified, the email address will be '
|
||||
'admin@<dns_domain>')),
|
||||
]
|
||||
|
||||
DESIGNATE_GROUP = 'designate'
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(designate_opts, DESIGNATE_GROUP)
|
||||
|
||||
|
||||
def get_clients(context):
|
||||
global _SESSION
|
||||
|
||||
if not _SESSION:
|
||||
_SESSION = session.Session()
|
||||
|
||||
auth = token_endpoint.Token(CONF.designate.url, context.auth_token)
|
||||
client = d_client.Client(session=_SESSION, auth=auth)
|
||||
admin_auth = password.Password(
|
||||
auth_url=CONF.designate.admin_auth_url,
|
||||
username=CONF.designate.admin_username,
|
||||
password=CONF.designate.admin_password,
|
||||
tenant_name=CONF.designate.admin_tenant_name,
|
||||
tenant_id=CONF.designate.admin_tenant_id)
|
||||
admin_client = d_client.Client(session=_SESSION, auth=admin_auth)
|
||||
return client, admin_client
|
||||
|
||||
|
||||
class Designate(driver.ExternalDNSService):
|
||||
"""Driver for Designate."""
|
||||
|
||||
def __init__(self):
|
||||
ipv4_ptr_zone_size = CONF.designate.ipv4_ptr_zone_prefix_size
|
||||
ipv6_ptr_zone_size = CONF.designate.ipv6_ptr_zone_prefix_size
|
||||
|
||||
if (ipv4_ptr_zone_size < IPV4_PTR_ZONE_PREFIX_MIN_SIZE or
|
||||
ipv4_ptr_zone_size > IPV4_PTR_ZONE_PREFIX_MAX_SIZE or
|
||||
(ipv4_ptr_zone_size % 8) != 0):
|
||||
raise dns.InvalidPTRZoneConfiguration(
|
||||
parameter='ipv4_ptr_zone_size', number='8',
|
||||
maximum=str(IPV4_PTR_ZONE_PREFIX_MAX_SIZE),
|
||||
minimum=str(IPV4_PTR_ZONE_PREFIX_MIN_SIZE))
|
||||
|
||||
if (ipv6_ptr_zone_size < IPV6_PTR_ZONE_PREFIX_MIN_SIZE or
|
||||
ipv6_ptr_zone_size > IPV6_PTR_ZONE_PREFIX_MAX_SIZE or
|
||||
(ipv6_ptr_zone_size % 4) != 0):
|
||||
raise dns.InvalidPTRZoneConfiguration(
|
||||
parameter='ipv6_ptr_zone_size', number='4',
|
||||
maximum=str(IPV6_PTR_ZONE_PREFIX_MAX_SIZE),
|
||||
minimum=str(IPV6_PTR_ZONE_PREFIX_MIN_SIZE))
|
||||
|
||||
def create_record_set(self, context, dns_domain, dns_name, records):
|
||||
designate, designate_admin = get_clients(context)
|
||||
v4, v6 = self._classify_records(records)
|
||||
try:
|
||||
if v4:
|
||||
designate.recordsets.create(dns_domain, dns_name, 'A', v4)
|
||||
if v6:
|
||||
designate.recordsets.create(dns_domain, dns_name, 'AAAA', v6)
|
||||
except d_exc.NotFound:
|
||||
raise dns.DNSDomainNotFound(dns_domain=dns_domain)
|
||||
except d_exc.Conflict:
|
||||
raise dns.DuplicateRecordSet(dns_name=dns_name)
|
||||
|
||||
if not CONF.designate.allow_reverse_dns_lookup:
|
||||
return
|
||||
# Set up the PTR records
|
||||
recordset_name = '%s.%s' % (dns_name, dns_domain)
|
||||
ptr_zone_email = 'admin@%s' % dns_domain[:-1]
|
||||
if CONF.designate.ptr_zone_email:
|
||||
ptr_zone_email = CONF.designate.ptr_zone_email
|
||||
for record in records:
|
||||
in_addr_name = netaddr.IPAddress(record).reverse_dns
|
||||
in_addr_zone_name = self._get_in_addr_zone_name(in_addr_name)
|
||||
in_addr_zone_description = (
|
||||
'An %s zone for reverse lookups set up by Neutron.' %
|
||||
'.'.join(in_addr_name.split('.')[-3:]))
|
||||
try:
|
||||
# Since we don't delete in-addr zones, assume it already
|
||||
# exists. If it doesn't, create it
|
||||
designate_admin.recordsets.create(in_addr_zone_name,
|
||||
in_addr_name, 'PTR',
|
||||
[recordset_name])
|
||||
except d_exc.NotFound:
|
||||
designate_admin.zones.create(
|
||||
in_addr_zone_name, email=ptr_zone_email,
|
||||
description=in_addr_zone_description)
|
||||
designate_admin.recordsets.create(in_addr_zone_name,
|
||||
in_addr_name, 'PTR',
|
||||
[recordset_name])
|
||||
|
||||
def _classify_records(self, records):
|
||||
v4 = []
|
||||
v6 = []
|
||||
for record in records:
|
||||
if netaddr.IPAddress(record).version == 4:
|
||||
v4.append(record)
|
||||
else:
|
||||
v6.append(record)
|
||||
return v4, v6
|
||||
|
||||
def _get_in_addr_zone_name(self, in_addr_name):
|
||||
units = self._get_bytes_or_nybles_to_skip(in_addr_name)
|
||||
return '.'.join(in_addr_name.split('.')[units:])
|
||||
|
||||
def _get_bytes_or_nybles_to_skip(self, in_addr_name):
|
||||
if 'in-addr.arpa' in in_addr_name:
|
||||
return (32 - CONF.designate.ipv4_ptr_zone_prefix_size) / 8
|
||||
return (128 - CONF.designate.ipv6_ptr_zone_prefix_size) / 4
|
||||
|
||||
def delete_record_set(self, context, dns_domain, dns_name, records):
|
||||
designate, designate_admin = get_clients(context)
|
||||
ids_to_delete = self._get_ids_ips_to_delete(
|
||||
dns_domain, '%s.%s' % (dns_name, dns_domain), records, designate)
|
||||
for _id in ids_to_delete:
|
||||
designate.recordsets.delete(dns_domain, _id)
|
||||
if not CONF.designate.allow_reverse_dns_lookup:
|
||||
return
|
||||
|
||||
for record in records:
|
||||
in_addr_name = netaddr.IPAddress(record).reverse_dns
|
||||
in_addr_zone_name = self._get_in_addr_zone_name(in_addr_name)
|
||||
designate_admin.recordsets.delete(in_addr_zone_name, in_addr_name)
|
||||
|
||||
def _get_ids_ips_to_delete(self, dns_domain, name, records,
|
||||
designate_client):
|
||||
try:
|
||||
recordsets = designate_client.recordsets.list(
|
||||
dns_domain, criterion={"name": "%s" % name})
|
||||
except d_exc.NotFound:
|
||||
raise dns.DNSDomainNotFound(dns_domain=dns_domain)
|
||||
ids = [rec['id'] for rec in recordsets]
|
||||
ips = [ip for rec in recordsets for ip in rec['records']]
|
||||
if set(ips) != set(records):
|
||||
raise dns.DuplicateRecordSet(dns_name=name)
|
||||
return ids
|
@ -23,6 +23,7 @@ from neutron.common import constants as n_const
|
||||
from neutron.common import rpc as n_rpc
|
||||
from neutron.common import topics
|
||||
from neutron.db import common_db_mixin
|
||||
from neutron.db import dns_db
|
||||
from neutron.db import extraroute_db
|
||||
from neutron.db import l3_db
|
||||
from neutron.db import l3_dvrscheduler_db
|
||||
@ -40,7 +41,8 @@ class L3RouterPlugin(service_base.ServicePluginBase,
|
||||
l3_hamode_db.L3_HA_NAT_db_mixin,
|
||||
l3_gwmode_db.L3_NAT_db_mixin,
|
||||
l3_dvrscheduler_db.L3_DVRsch_db_mixin,
|
||||
l3_hascheduler_db.L3_HA_scheduler_db_mixin):
|
||||
l3_hascheduler_db.L3_HA_scheduler_db_mixin,
|
||||
dns_db.DNSDbMixin):
|
||||
|
||||
"""Implementation of the Neutron L3 Router Service Plugin.
|
||||
|
||||
@ -53,7 +55,8 @@ class L3RouterPlugin(service_base.ServicePluginBase,
|
||||
"""
|
||||
supported_extension_aliases = ["dvr", "router", "ext-gw-mode",
|
||||
"extraroute", "l3_agent_scheduler",
|
||||
"l3-ha", "router_availability_zone"]
|
||||
"l3-ha", "router_availability_zone",
|
||||
"dns-integration"]
|
||||
|
||||
@resource_registry.tracked_resources(router=l3_db.Router,
|
||||
floatingip=l3_db.FloatingIP)
|
||||
|
@ -242,7 +242,8 @@ class L3DvrTestCase(ml2_test_base.ML2TestFramework):
|
||||
floating_ip = {'floating_network_id': ext_net_id,
|
||||
'router_id': router['id'],
|
||||
'port_id': int_port['port']['id'],
|
||||
'tenant_id': int_port['port']['tenant_id']}
|
||||
'tenant_id': int_port['port']['tenant_id'],
|
||||
'dns_name': '', 'dns_domain': ''}
|
||||
with mock.patch.object(
|
||||
self.l3_plugin, '_l3_rpc_notifier') as l3_notif:
|
||||
self.l3_plugin.create_floatingip(
|
||||
@ -307,7 +308,8 @@ class L3DvrTestCase(ml2_test_base.ML2TestFramework):
|
||||
floating_ip = {'floating_network_id': ext_net_id,
|
||||
'router_id': router1['id'],
|
||||
'port_id': int_port1['port']['id'],
|
||||
'tenant_id': int_port1['port']['tenant_id']}
|
||||
'tenant_id': int_port1['port']['tenant_id'],
|
||||
'dns_name': '', 'dns_domain': ''}
|
||||
floating_ip = self.l3_plugin.create_floatingip(
|
||||
self.context, {'floatingip': floating_ip})
|
||||
|
||||
@ -365,7 +367,8 @@ class L3DvrTestCase(ml2_test_base.ML2TestFramework):
|
||||
floating_ip = {'floating_network_id': ext_net_id,
|
||||
'router_id': router['id'],
|
||||
'port_id': int_port['port']['id'],
|
||||
'tenant_id': int_port['port']['tenant_id']}
|
||||
'tenant_id': int_port['port']['tenant_id'],
|
||||
'dns_name': '', 'dns_domain': ''}
|
||||
floating_ip = self.l3_plugin.create_floatingip(
|
||||
self.context, {'floatingip': floating_ip})
|
||||
with mock.patch.object(
|
||||
|
@ -33,15 +33,18 @@ from neutron.callbacks import registry
|
||||
from neutron.callbacks import resources
|
||||
from neutron.common import constants as l3_constants
|
||||
from neutron.common import exceptions as n_exc
|
||||
from neutron.common import utils
|
||||
from neutron import context
|
||||
from neutron.db import common_db_mixin
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.db import dns_db
|
||||
from neutron.db import external_net_db
|
||||
from neutron.db import l3_agentschedulers_db
|
||||
from neutron.db import l3_attrs_db
|
||||
from neutron.db import l3_db
|
||||
from neutron.db import l3_dvr_db
|
||||
from neutron.db import l3_dvrscheduler_db
|
||||
from neutron.extensions import dns
|
||||
from neutron.extensions import external_net
|
||||
from neutron.extensions import l3
|
||||
from neutron.extensions import portbindings
|
||||
@ -263,9 +266,9 @@ class TestL3NatBasePlugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
|
||||
# This plugin class is for tests with plugin that integrates L3.
|
||||
class TestL3NatIntPlugin(TestL3NatBasePlugin,
|
||||
l3_db.L3_NAT_db_mixin):
|
||||
l3_db.L3_NAT_db_mixin, dns_db.DNSDbMixin):
|
||||
|
||||
supported_extension_aliases = ["external-net", "router"]
|
||||
supported_extension_aliases = ["external-net", "router", "dns-integration"]
|
||||
|
||||
|
||||
# This plugin class is for tests with plugin that integrates L3 and L3 agent
|
||||
@ -293,9 +296,9 @@ class TestNoL3NatPlugin(TestL3NatBasePlugin):
|
||||
# delegate away L3 routing functionality
|
||||
class TestL3NatServicePlugin(common_db_mixin.CommonDbMixin,
|
||||
l3_dvr_db.L3_NAT_with_dvr_db_mixin,
|
||||
l3_db.L3_NAT_db_mixin):
|
||||
l3_db.L3_NAT_db_mixin, dns_db.DNSDbMixin):
|
||||
|
||||
supported_extension_aliases = ["router"]
|
||||
supported_extension_aliases = ["router", "dns-integration"]
|
||||
|
||||
def get_plugin_type(self):
|
||||
return service_constants.L3_ROUTER_NAT
|
||||
@ -1190,6 +1193,9 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
|
||||
expected_port_update = {
|
||||
'device_owner': l3_constants.DEVICE_OWNER_ROUTER_INTF,
|
||||
'device_id': r['router']['id']}
|
||||
plugin = manager.NeutronManager.get_plugin()
|
||||
if utils.is_extension_supported(plugin, dns.Dns.get_alias()):
|
||||
expected_port_update['dns_name'] = ''
|
||||
update_port.assert_called_with(
|
||||
mock.ANY, p['port']['id'], {'port': expected_port_update})
|
||||
# fetch port and confirm device_id
|
||||
|
@ -558,7 +558,8 @@ class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase):
|
||||
l3plugin.create_floatingip(
|
||||
context.get_admin_context(),
|
||||
{'floatingip': {'floating_network_id': n['network']['id'],
|
||||
'tenant_id': n['network']['tenant_id']}}
|
||||
'tenant_id': n['network']['tenant_id'],
|
||||
'dns_name': '', 'dns_domain': ''}}
|
||||
)
|
||||
self._delete('networks', n['network']['id'])
|
||||
flips = l3plugin.get_floatingips(context.get_admin_context())
|
||||
|
@ -0,0 +1,14 @@
|
||||
---
|
||||
prelude: >
|
||||
Support integration with external DNS service.
|
||||
features:
|
||||
- Floating IPs can have dns_name and dns_domain attributes associated
|
||||
with them
|
||||
- Ports can have a dns_name attribute associated with them. The network
|
||||
where a port is created can have a dns_domain associated with it
|
||||
- Floating IPs and ports will be published in an external DNS service
|
||||
if they have dns_name and dns_domain attributes associated with them.
|
||||
- The reference driver integrates neutron with designate
|
||||
- Drivers for other DNSaaS can be implemented
|
||||
- Driver is configured in the default section of neutron.conf using
|
||||
parameter 'external_dns_driver'
|
@ -43,3 +43,4 @@ oslo.versionedobjects>=0.13.0 # Apache-2.0
|
||||
ovs>=2.4.0;python_version=='2.7' # Apache-2.0
|
||||
|
||||
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
|
||||
python-designateclient>=1.5.0 # Apache-2.0
|
||||
|
@ -100,6 +100,7 @@ neutron.ml2.extension_drivers =
|
||||
testdb = neutron.tests.unit.plugins.ml2.drivers.ext_test:TestDBExtensionDriver
|
||||
port_security = neutron.plugins.ml2.extensions.port_security:PortSecurityExtensionDriver
|
||||
qos = neutron.plugins.ml2.extensions.qos:QosExtensionDriver
|
||||
dns = neutron.plugins.ml2.extensions.dns_integration:DNSExtensionDriverML2
|
||||
neutron.openstack.common.cache.backends =
|
||||
memory = neutron.openstack.common.cache._backends.memory:MemoryBackend
|
||||
neutron.ipam_drivers =
|
||||
@ -112,6 +113,8 @@ neutron.qos.agent_drivers =
|
||||
sriov = neutron.plugins.ml2.drivers.mech_sriov.agent.extension_drivers.qos_driver:QosSRIOVAgentDriver
|
||||
neutron.agent.linux.pd_drivers =
|
||||
dibbler = neutron.agent.linux.dibbler:PDDibbler
|
||||
neutron.services.external_dns_drivers =
|
||||
designate = neutron.services.externaldns.drivers.designate.driver:Designate
|
||||
# These are for backwards compat with Icehouse notification_driver configuration values
|
||||
# TODO(mriedem): Remove these once liberty-eol happens.
|
||||
oslo.messaging.notify.drivers =
|
||||
|
Loading…
Reference in New Issue
Block a user