diff --git a/README.md b/README.md index 82c158a4..af4d8d85 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.yaml b/config.yaml index cf791548..2b487e3c 100755 --- a/config.yaml +++ b/config.yaml @@ -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.'. diff --git a/hooks/neutron_api_context.py b/hooks/neutron_api_context.py index cde51a26..0484fba9 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -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}(?= 'mitaka': diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index e5fe66f3..36a9a3cb 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -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, @@ -502,6 +503,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: diff --git a/templates/kilo/ml2_conf.ini b/templates/kilo/ml2_conf.ini index 8b06d39f..8981f5e7 100644 --- a/templates/kilo/ml2_conf.ini +++ b/templates/kilo/ml2_conf.ini @@ -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' -%} diff --git a/templates/mitaka/ml2_conf.ini b/templates/mitaka/ml2_conf.ini index 022b1723..bd4bd5e6 100644 --- a/templates/mitaka/ml2_conf.ini +++ b/templates/mitaka/ml2_conf.ini @@ -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' -%} diff --git a/templates/mitaka/neutron.conf b/templates/mitaka/neutron.conf index 3a003a22..ffbee1d3 100644 --- a/templates/mitaka/neutron.conf +++ b/templates/mitaka/neutron.conf @@ -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 }} diff --git a/templates/newton/neutron.conf b/templates/newton/neutron.conf index 327127a0..fb0aa89d 100644 --- a/templates/newton/neutron.conf +++ b/templates/newton/neutron.conf @@ -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 }} diff --git a/unit_tests/test_neutron_api_context.py b/unit_tests/test_neutron_api_context.py index 2e22ca9a..1c5724c4 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -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() diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index 51361439..9d381e3a 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -60,6 +60,7 @@ TO_PATCH = [ 'l3ha_router_present', 'execd_preinstall', 'filter_installed_packages', + 'get_dns_domain', 'get_dvr', 'get_l3ha', 'get_l2population', @@ -538,6 +539,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 = { @@ -620,6 +622,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, @@ -653,6 +656,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, @@ -686,6 +690,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, @@ -721,6 +726,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,