Enable internal DNS resolution

Add the dns-domain config and enable-ml2-dns options, allowing the
user to enable DNS integration between Neutron and Nova. This enables
the DNS integration between Nova and Neutron for internal DNS services
when the enable-ml2-dns option is set to True.

Change-Id: Id5f828da003e056a882297ffdbf3df22e856d14a
Implements: blueprint internal-dns
This commit is contained in:
Billy Olsen 2017-04-18 23:22:14 -07:00
parent 15dbc5fc3d
commit 9bbd2bad9c
10 changed files with 230 additions and 9 deletions

View File

@ -66,6 +66,31 @@ os-{admin,internal,public}-hostname(s) are set
This charm only support deployment with OpenStack Icehouse or better.
# Internal DNS for Cloud Guests
The charm supports enabling internal DNS resolution for cloud guests in
accordance with the OpenStack DNS integration guide. To enable internal
DNS resolution, the 'enable-ml2-dns' option must be set to True. When
enabled, the domain name specified in the 'dns-domain' will be advertised
as the nameserver search path by the dhcp agents.
The Nova compute service will leverage this functionality when enabled.
When ports are allocated by the compute service, the dns_name of the port
is populated with a DNS sanitized version of the instance's display name.
The Neutron DHCP agents will then create host entries in the dnsmasq's
configuration files matching the dns_name of the port to the IP address
associated with the port.
Note that the DNS nameserver provided to the instance by the DHCP agent
depends on the tenant's network setup. The Neutron DHCP agent only advertises
itself as a nameserver when the Neutron subnet does not have nameservers
configured. If additional nameservers are needed and internal DNS is desired,
then the IP address of the DHCP port should be added to the subnet's
list of configured nameservers.
For more information refer to the OpenStack documentation on
[DNS Integration](https://docs.openstack.org/ocata/networking-guide/config-dns-int.html).
# Network Space support
This charm supports the use of Juju Network Spaces, allowing the charm

View File

@ -550,6 +550,13 @@ options:
type: boolean
default: False
description: Enable port security extension for ML2 plugin (>= kilo).
enable-ml2-dns:
type: boolean
default: False
description: |
Enables the Neutron DNS extension driver (>= mitaka). When enabled,
ports attached to Nova instances will have DNS names assigned based
on the instance name.
haproxy-server-timeout:
type: int
default:
@ -649,3 +656,12 @@ options:
when using an overlay/tunnel protocol. This option allows
specifying a physical network MTU value that differs from the
default global-physnet-mtu value.
dns-domain:
type: string
default: openstack.example.
description: |
Specifies the dns domain name that should be used for building instance
hostnames. An empty option or the value of 'openstacklocal' will cause
the dhcp agents to broadcast the default domain of openstacklocal and
will not enable internal cloud dns resolution. This value should end
with a '.', e.g. 'cloud.example.org.'.

View File

@ -13,6 +13,7 @@
# limitations under the License.
import ast
import re
from collections import OrderedDict
@ -22,6 +23,8 @@ from charmhelpers.core.hookenv import (
related_units,
relation_get,
log,
DEBUG,
ERROR,
)
from charmhelpers.contrib.openstack import context
from charmhelpers.contrib.hahelpers.cluster import (
@ -42,6 +45,16 @@ OVERLAY_NET_TYPES = [VXLAN, GRE]
NON_OVERLAY_NET_TYPES = [VLAN, FLAT, LOCAL]
TENANT_NET_TYPES = [VXLAN, GRE, VLAN, FLAT, LOCAL]
EXTENSION_DRIVER_PORT_SECURITY = 'port_security'
EXTENSION_DRIVER_DNS = 'dns'
# Domain name validation regex which is used to certify that
# the domain-name consists only of valid characters, is not
# longer than 63 characters in length for any name segment,
# and each segment does not begin or end with a hyphen.
DOMAIN_NAME_REGEX = re.compile(r'^(?!-)[A-Z\d-]{1,63}(?<!-)$',
re.IGNORECASE)
def get_l2population():
plugin = config('neutron-plugin')
@ -121,6 +134,44 @@ def get_dvr():
return False
def get_dns_domain():
if not config('enable-ml2-dns'):
log('ML2 DNS Extensions are not enabled.', DEBUG)
return ""
dns_domain = config('dns-domain')
if not dns_domain:
log('No dns-domain has been configured', DEBUG)
return dns_domain
release = os_release('neutron-server')
if CompareOpenStackReleases(release) < 'mitaka':
log('Internal DNS resolution is not supported before Mitaka')
return ""
# Strip any trailing . at the end
if dns_domain[-1] == '.':
dns_domain = dns_domain[:-1]
# Ensure that the dns name is only a valid name. Valid entries include
# a-z, A-Z, 0-9, ., and -. No particular name may be longer than 63
# characters, each part cannot begin/end with a -. Validate this here in
# order to prevent other chaos which may prevent neutron services from
# functioning properly.
# Note: intentionally not validating the length of the domain name because
# this is practically difficult to validate reasonably well.
for level in dns_domain.split('.'):
if not DOMAIN_NAME_REGEX.match(level):
msg = "dns-domain '%s' is an invalid domain name." % dns_domain
log(msg, ERROR)
raise ValueError(msg)
# Make sure it ends with a .
dns_domain += '.'
return dns_domain
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
@ -303,7 +354,17 @@ class NeutronCCContext(context.NeutronContext):
if vni_ranges:
ctxt['vni_ranges'] = ','.join(vni_ranges.split())
ctxt['enable_ml2_port_security'] = config('enable-ml2-port-security')
extension_drivers = []
if config('enable-ml2-port-security'):
extension_drivers.append(EXTENSION_DRIVER_PORT_SECURITY)
dns_domain = get_dns_domain()
if dns_domain:
extension_drivers.append(EXTENSION_DRIVER_DNS)
ctxt['dns_domain'] = dns_domain
if extension_drivers:
ctxt['extension_drivers'] = ','.join(extension_drivers)
ctxt['enable_sriov'] = config('enable-sriov')
if cmp_release == 'kilo' or cmp_release >= 'mitaka':

View File

@ -87,6 +87,7 @@ from neutron_api_utils import (
setup_ipv6,
)
from neutron_api_context import (
get_dns_domain,
get_dvr,
get_l3ha,
get_l2population,
@ -504,6 +505,10 @@ def neutron_plugin_api_relation_joined(rid=None):
'region': config('region'),
})
dns_domain = get_dns_domain()
if dns_domain:
relation_data['dns-domain'] = dns_domain
if is_api_ready(CONFIGS):
relation_data['neutron-api-ready'] = "yes"
else:

View File

@ -4,8 +4,8 @@
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[ml2]
{% if enable_ml2_port_security -%}
extension_drivers=port_security
{% if extension_drivers -%}
extension_drivers={{ extension_drivers }}
{% endif -%}
{% if neutron_plugin == 'Calico' -%}

View File

@ -4,8 +4,8 @@
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[ml2]
{% if enable_ml2_port_security -%}
extension_drivers=port_security
{% if extension_drivers -%}
extension_drivers={{ extension_drivers }}
{% endif -%}
{% if neutron_plugin == 'Calico' -%}

View File

@ -18,6 +18,10 @@ rpc_workers = {{ workers }}
router_distributed = {{ enable_dvr }}
{% if dns_domain -%}
dns_domain = {{ dns_domain }}
{% endif -%}
l3_ha = {{ l3_ha }}
{% if l3_ha -%}
max_l3_agents_per_router = {{ max_l3_agents_per_router }}

View File

@ -18,6 +18,10 @@ rpc_workers = {{ workers }}
router_distributed = {{ enable_dvr }}
{% if dns_domain -%}
dns_domain = {{ dns_domain }}
{% endif -%}
l3_ha = {{ l3_ha }}
{% if l3_ha -%}
max_l3_agents_per_router = {{ max_l3_agents_per_router }}

View File

@ -203,6 +203,27 @@ class GeneralTests(CharmTestCase):
self.os_release.return_value = 'juno'
self.assertEquals(context.get_dvr(), False)
def test_get_dns_domain(self):
self.test_config.set('dns-domain', 'example.org.')
self.test_config.set('enable-ml2-dns', True)
self.os_release.return_value = 'mitaka'
self.assertEquals(context.get_dns_domain(), 'example.org.')
def test_get_dns_domain_bad_values(self):
self.os_release.return_value = 'mitaka'
self.test_config.set('enable-ml2-dns', True)
bad_values = ['example@foo.org',
'exclamation!marks.notwelcom.ed',
'%s.way.too.long' % ('x' * 64),
'-hyphen.in.front',
'hypen-.in.back',
'no_.under_scor.es',
]
for value in bad_values:
self.test_config.set('dns-domain', value)
self.assertRaises(ValueError, context.get_dns_domain)
class IdentityServiceContext(CharmTestCase):
@ -385,7 +406,7 @@ class NeutronCCContextTest(CharmTestCase):
'quota_vip': 10,
'vlan_ranges': 'physnet1:1000:2000',
'vni_ranges': '1001:2000',
'enable_ml2_port_security': True,
'extension_drivers': 'port_security',
'enable_hyperv': False
}
napi_ctxt = context.NeutronCCContext()
@ -393,6 +414,50 @@ class NeutronCCContextTest(CharmTestCase):
with patch.object(napi_ctxt, '_ensure_packages'):
self.assertEquals(ctxt_data, napi_ctxt())
@patch.object(context.NeutronCCContext, 'network_manager')
@patch.object(context.NeutronCCContext, 'plugin')
def test_neutroncc_context_dns_setting(self, plugin, nm):
plugin.return_value = None
self.test_config.set('enable-ml2-dns', True)
self.test_config.set('dns-domain', 'example.org.')
self.os_release.return_value = 'mitaka'
napi_ctxt = context.NeutronCCContext()
with patch.object(napi_ctxt, '_ensure_packages'):
ctxt = napi_ctxt()
self.assertEqual('example.org.', ctxt['dns_domain'])
self.assertEqual('port_security,dns', ctxt['extension_drivers'])
@patch.object(context.NeutronCCContext, 'network_manager')
@patch.object(context.NeutronCCContext, 'plugin')
def test_neutroncc_context_dns_no_port_security_setting(self,
plugin, nm):
"""Verify extension drivers without port security."""
plugin.return_value = None
self.test_config.set('enable-ml2-port-security', False)
self.test_config.set('enable-ml2-dns', True)
self.test_config.set('dns-domain', 'example.org.')
self.os_release.return_value = 'mitaka'
napi_ctxt = context.NeutronCCContext()
with patch.object(napi_ctxt, '_ensure_packages'):
ctxt = napi_ctxt()
self.assertEquals('example.org.', ctxt['dns_domain'])
self.assertEquals('dns', ctxt['extension_drivers'])
@patch.object(context.NeutronCCContext, 'network_manager')
@patch.object(context.NeutronCCContext, 'plugin')
def test_neutroncc_context_dns_kilo(self, plugin, nm):
"""Verify dns extension and domain are not specified in kilo."""
plugin.return_value = None
self.test_config.set('enable-ml2-port-security', False)
self.test_config.set('enable-ml2-dns', True)
self.test_config.set('dns-domain', 'example.org.')
self.os_release.return_value = 'kilo'
napi_ctxt = context.NeutronCCContext()
with patch.object(napi_ctxt, '_ensure_packages'):
ctxt = napi_ctxt()
self.assertFalse('dns_domain' in ctxt)
self.assertFalse('extension_drivers' in ctxt)
@patch.object(context.NeutronCCContext, 'network_manager')
@patch.object(context.NeutronCCContext, 'plugin')
@patch('__builtin__.__import__')
@ -427,7 +492,7 @@ class NeutronCCContextTest(CharmTestCase):
'vlan_ranges': 'physnet1:1000:2000',
'vni_ranges': '1001:2000,3001:4000',
'network_providers': 'physnet2,physnet3',
'enable_ml2_port_security': True,
'extension_drivers': 'port_security',
'enable_hyperv': False
}
napi_ctxt = context.NeutronCCContext()
@ -472,7 +537,7 @@ class NeutronCCContextTest(CharmTestCase):
'quota_vip': 10,
'vlan_ranges': 'physnet1:1000:2000',
'vni_ranges': '1001:2000',
'enable_ml2_port_security': True,
'extension_drivers': 'port_security',
'enable_hyperv': False
}
napi_ctxt = context.NeutronCCContext()
@ -524,7 +589,7 @@ class NeutronCCContextTest(CharmTestCase):
'quota_vip': 10,
'vlan_ranges': 'physnet1:1000:2000',
'vni_ranges': '1001:2000',
'enable_ml2_port_security': True,
'extension_drivers': 'port_security',
'enable_hyperv': False
}
napi_ctxt = context.NeutronCCContext()

View File

@ -60,6 +60,7 @@ TO_PATCH = [
'l3ha_router_present',
'execd_preinstall',
'filter_installed_packages',
'get_dns_domain',
'get_dvr',
'get_l3ha',
'get_l2population',
@ -539,6 +540,7 @@ class NeutronAPIHooksTests(CharmTestCase):
port = 1234
_canonical_url.return_value = host
self.api_port.return_value = port
self.get_dns_domain.return_value = ""
self.is_relation_made = True
neutron_url = '%s:%s' % (host, port)
_relation_data = {
@ -621,6 +623,7 @@ class NeutronAPIHooksTests(CharmTestCase):
self.get_l3ha.return_value = False
self.get_l2population.return_value = False
self.get_overlay_network_type.return_value = 'vxlan'
self.get_dns_domain.return_value = ''
self._call_hook('neutron-plugin-api-relation-joined')
self.relation_set.assert_called_with(
relation_id=None,
@ -654,6 +657,7 @@ class NeutronAPIHooksTests(CharmTestCase):
self.get_l3ha.return_value = False
self.get_l2population.return_value = True
self.get_overlay_network_type.return_value = 'vxlan'
self.get_dns_domain.return_value = ''
self._call_hook('neutron-plugin-api-relation-joined')
self.relation_set.assert_called_with(
relation_id=None,
@ -687,6 +691,7 @@ class NeutronAPIHooksTests(CharmTestCase):
self.get_l3ha.return_value = True
self.get_l2population.return_value = False
self.get_overlay_network_type.return_value = 'vxlan'
self.get_dns_domain.return_value = ''
self._call_hook('neutron-plugin-api-relation-joined')
self.relation_set.assert_called_with(
relation_id=None,
@ -722,6 +727,42 @@ class NeutronAPIHooksTests(CharmTestCase):
self.get_l3ha.return_value = True
self.get_l2population.return_value = False
self.get_overlay_network_type.return_value = 'vxlan'
self.get_dns_domain.return_value = ''
self._call_hook('neutron-plugin-api-relation-joined')
self.relation_set.assert_called_with(
relation_id=None,
**_relation_data
)
def test_neutron_plugin_api_relation_joined_dns(self):
self.unit_get.return_value = '172.18.18.18'
self.IdentityServiceContext.return_value = \
DummyContext(return_value={})
_relation_data = {
'neutron-security-groups': False,
'enable-dvr': False,
'enable-l3ha': False,
'addr': '172.18.18.18',
'l2-population': False,
'overlay-network-type': 'vxlan',
'service_protocol': None,
'auth_protocol': None,
'service_tenant': None,
'service_port': None,
'region': 'RegionOne',
'service_password': None,
'auth_port': None,
'auth_host': None,
'service_username': None,
'service_host': None,
'neutron-api-ready': 'no',
'dns-domain': 'openstack.example.'
}
self.get_dvr.return_value = False
self.get_l3ha.return_value = False
self.get_l2population.return_value = False
self.get_overlay_network_type.return_value = 'vxlan'
self.get_dns_domain.return_value = 'openstack.example.'
self._call_hook('neutron-plugin-api-relation-joined')
self.relation_set.assert_called_with(
relation_id=None,