diff --git a/config.yaml b/config.yaml index f4b5e732..580a8582 100644 --- a/config.yaml +++ b/config.yaml @@ -285,6 +285,28 @@ options: juju-myservice-0 If you're running multiple environments with the same services in them this allows you to differentiate between them. + enable-dvr: + default: False + type: boolean + description: | + Enable Distributed Virtual Routing (juno and above). + enable-l3ha: + default: False + type: boolean + description: | + Enable L3 HA (juno and above). + max-l3-agents-per-router: + default: 2 + type: int + description: | + Maximum number of l3 agents to host a router. Only used when enable-l3ha + is True + min-l3-agents-per-router: + default: 2 + type: int + description: | + Minimum number of l3 agents to host a router. Only used when enable-l3ha + is True nagios_servicegroups: default: "" type: string diff --git a/hooks/neutron_api_context.py b/hooks/neutron_api_context.py index 2c488803..f0ff0d33 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -3,12 +3,16 @@ from charmhelpers.core.hookenv import ( relation_ids, related_units, relation_get, + log, ) from charmhelpers.contrib.openstack import context from charmhelpers.contrib.hahelpers.cluster import ( determine_api_port, determine_apache_port, ) +from charmhelpers.contrib.openstack.utils import ( + os_release, +) def get_l2population(): @@ -23,6 +27,43 @@ def get_overlay_network_type(): return overlay_net +def get_l3ha(): + if config('enable-l3ha'): + if os_release('neutron-server') < 'juno': + log('Disabling L3 HA, enable-l3ha is not valid before Juno') + return False + if config('overlay-network-type') not in ['vlan', 'gre', 'vxlan']: + log('Disabling L3 HA, enable-l3ha requires the use of the vxlan, ' + 'vlan or gre overlay network') + return False + if get_l2population(): + log('Disabling L3 HA, l2-population must be disabled with L3 HA') + return False + return True + else: + return False + + +def get_dvr(): + if config('enable-dvr'): + if os_release('neutron-server') < 'juno': + log('Disabling DVR, enable-dvr is not valid before Juno') + return False + if config('overlay-network-type') != 'vxlan': + log('Disabling DVR, enable-dvr requires the use of the vxlan ' + 'overlay network') + return False + if get_l3ha(): + log('Disabling DVR, enable-l3ha must be disabled with dvr') + return False + if not get_l2population(): + log('Disabling DVR, l2-population must be enabled to use dvr') + return False + return True + else: + return False + + class ApacheSSLContext(context.ApacheSSLContext): interfaces = ['https'] @@ -69,6 +110,14 @@ class NeutronCCContext(context.NeutronContext): def neutron_overlay_network_type(self): return get_overlay_network_type() + @property + def neutron_dvr(self): + return get_dvr() + + @property + def neutron_l3ha(self): + return get_l3ha() + # Do not need the plugin agent installed on the api server def _ensure_packages(self): pass @@ -91,6 +140,13 @@ class NeutronCCContext(context.NeutronContext): ctxt['nsx_controllers_list'] = \ config('nsx-controllers').split() ctxt['l2_population'] = self.neutron_l2_population + ctxt['enable_dvr'] = self.neutron_dvr + ctxt['l3_ha'] = self.neutron_l3ha + if self.neutron_l3ha: + ctxt['max_l3_agents_per_router'] = \ + config('max-l3-agents-per-router') + ctxt['min_l3_agents_per_router'] = \ + config('min-l3-agents-per-router') ctxt['overlay_network_type'] = self.neutron_overlay_network_type ctxt['external_network'] = config('neutron-external-network') ctxt['verbose'] = config('verbose') diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index 5c52b3e9..56361049 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -2,7 +2,6 @@ import sys import uuid - from subprocess import check_call from charmhelpers.core.hookenv import ( Hooks, @@ -40,12 +39,16 @@ from neutron_api_utils import ( determine_packages, determine_ports, do_openstack_upgrade, + dvr_router_present, + l3ha_router_present, register_configs, restart_map, services, setup_ipv6 ) from neutron_api_context import ( + get_dvr, + get_l3ha, get_l2population, get_overlay_network_type, ) @@ -110,6 +113,16 @@ def install(): @hooks.hook('config-changed') @restart_on_change(restart_map(), stopstart=True) def config_changed(): + if l3ha_router_present() and not get_l3ha(): + e = ('Cannot disable Router HA while ha enabled routers exist. Please' + ' remove any ha routers') + log(e, level=ERROR) + raise Exception(e) + if dvr_router_present() and not get_dvr(): + e = ('Cannot disable dvr while dvr enabled routers exist. Please' + ' remove any distributed routers') + log(e, level=ERROR) + raise Exception(e) apt_install(filter_installed_packages( determine_packages(config('openstack-origin'))), fatal=True) @@ -278,6 +291,8 @@ def neutron_plugin_api_relation_joined(rid=None): relation_data = { 'neutron-security-groups': config('neutron-security-groups'), 'l2-population': get_l2population(), + 'enable-dvr': get_dvr(), + 'enable-l3ha': get_l3ha(), 'overlay-network-type': get_overlay_network_type(), } diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index 90d9a036..97ab71c9 100644 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -1,5 +1,6 @@ from collections import OrderedDict from copy import deepcopy +from functools import partial import os from base64 import b64encode from charmhelpers.contrib.openstack import context, templating @@ -244,3 +245,28 @@ def setup_ipv6(): ' main') apt_update() apt_install('haproxy/trusty-backports', fatal=True) + + +def router_feature_present(feature): + ''' Check For dvr enabled routers ''' + env = neutron_api_context.IdentityServiceContext()() + if not env: + log('Unable to check resources at this time') + return + + auth_url = '%(auth_protocol)s://%(auth_host)s:%(auth_port)s/v2.0' % env + # Late import to avoid install hook failures when pkg hasnt been installed + from neutronclient.v2_0 import client + neutron_client = client.Client(username=env['admin_user'], + password=env['admin_password'], + tenant_name=env['admin_tenant_name'], + auth_url=auth_url, + region_name=env['region']) + for router in neutron_client.list_routers()['routers']: + if router.get(feature, False): + return True + return False + +l3ha_router_present = partial(router_feature_present, feature='ha') + +dvr_router_present = partial(router_feature_present, feature='distributed') diff --git a/templates/juno/neutron.conf b/templates/juno/neutron.conf new file mode 100644 index 00000000..b3d7d508 --- /dev/null +++ b/templates/juno/neutron.conf @@ -0,0 +1,85 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +## Restart trigger {{ restart_trigger }} +############################################################################### +[DEFAULT] +verbose = {{ verbose }} +debug = {{ debug }} +use_syslog = {{ use_syslog }} +state_path = /var/lib/neutron +lock_path = $state_path/lock +bind_host = {{ bind_host }} +auth_strategy = keystone +notification_driver = neutron.openstack.common.notifier.rpc_notifier +api_workers = {{ workers }} +rpc_workers = {{ workers }} + +router_distributed = {{ enable_dvr }} + +l3_ha = {{ l3_ha }} +{% if l3_ha -%} +max_l3_agents_per_router = {{ max_l3_agents_per_router }} +min_l3_agents_per_router = {{ min_l3_agents_per_router }} +{% endif -%} + +{% if neutron_bind_port -%} +bind_port = {{ neutron_bind_port }} +{% else -%} +bind_port = 9696 +{% endif -%} + +{% if core_plugin -%} +core_plugin = {{ core_plugin }} +{% if neutron_plugin in ['ovs', 'ml2'] -%} +service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,neutron.services.firewall.fwaas_plugin.FirewallPlugin,neutron.services.loadbalancer.plugin.LoadBalancerPlugin,neutron.services.vpn.plugin.VPNDriverPlugin,neutron.services.metering.metering_plugin.MeteringPlugin +{% endif -%} +{% endif -%} + +{% if neutron_security_groups -%} +allow_overlapping_ips = True +neutron_firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver +{% endif -%} + +{% include "parts/rabbitmq" %} + +notify_nova_on_port_status_changes = True +notify_nova_on_port_data_changes = True +nova_url = {{ nova_url }} +nova_region_name = {{ region }} +{% if auth_host -%} +nova_admin_username = {{ admin_user }} +nova_admin_tenant_id = {{ admin_tenant_id }} +nova_admin_password = {{ admin_password }} +nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 +{% endif -%} + +[quotas] +quota_driver = neutron.db.quota_db.DbQuotaDriver +{% if neutron_security_groups -%} +quota_items = network,subnet,port,security_group,security_group_rule +{% endif -%} + +[agent] +root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf + +[keystone_authtoken] +signing_dir = /var/lib/neutron/keystone-signing +{% if service_host -%} +service_protocol = {{ service_protocol }} +service_host = {{ service_host }} +service_port = {{ service_port }} +auth_host = {{ auth_host }} +auth_port = {{ auth_port }} +auth_protocol = {{ auth_protocol }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +{% endif -%} + +{% include "parts/section-database" %} + +[service_providers] +service_provider=LOADBALANCER:Haproxy:neutron.services.loadbalancer.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default +service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default +service_provider=FIREWALL:Iptables:neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver:default diff --git a/templates/kilo/neutron.conf b/templates/kilo/neutron.conf index 123904b9..b380247c 100644 --- a/templates/kilo/neutron.conf +++ b/templates/kilo/neutron.conf @@ -14,6 +14,14 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier api_workers = {{ workers }} rpc_workers = {{ workers }} +router_distributed = {{ enable_dvr }} + +l3_ha = {{ l3_ha }} +{% if l3_ha -%} +max_l3_agents_per_router = {{ max_l3_agents_per_router }} +min_l3_agents_per_router = {{ min_l3_agents_per_router }} +{% endif -%} + {% if neutron_bind_port -%} bind_port = {{ neutron_bind_port }} {% else -%} diff --git a/unit_tests/test_neutron_api_context.py b/unit_tests/test_neutron_api_context.py index 60134bac..8fd91af4 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -3,15 +3,131 @@ from mock import patch import neutron_api_context as context import charmhelpers TO_PATCH = [ + 'config', + 'determine_api_port', + 'determine_apache_port', + 'log', + 'os_release', 'relation_get', 'relation_ids', 'related_units', - 'config', - 'determine_api_port', - 'determine_apache_port' ] +class GeneralTests(CharmTestCase): + def setUp(self): + super(GeneralTests, self).setUp(context, TO_PATCH) + self.relation_get.side_effect = self.test_relation.get + self.config.side_effect = self.test_config.get + + def test_l2population(self): + self.test_config.set('l2-population', True) + self.test_config.set('neutron-plugin', 'ovs') + self.assertEquals(context.get_l2population(), True) + + def test_l2population_nonovs(self): + self.test_config.set('l2-population', True) + self.test_config.set('neutron-plugin', 'nsx') + self.assertEquals(context.get_l2population(), False) + + def test_get_overlay_network_type(self): + self.test_config.set('overlay-network-type', 'gre') + self.assertEquals(context.get_overlay_network_type(), 'gre') + + def test_get_overlay_network_type_unsupported(self): + self.test_config.set('overlay-network-type', 'tokenring') + with self.assertRaises(Exception) as _exceptctxt: + context.get_overlay_network_type() + self.assertEqual(_exceptctxt.exception.message, + 'Unsupported overlay-network-type') + + def test_get_l3ha(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), True) + + def test_get_l3ha_prejuno(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'icehouse' + self.assertEquals(context.get_l3ha(), False) + + def test_get_l3ha_l2pop(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), False) + + def test_get_l3ha_badoverlay(self): + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'tokenring') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_l3ha(), False) + + def test_get_dvr(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), True) + + def test_get_dvr_explicit_off(self): + self.test_config.set('enable-dvr', False) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_prejuno(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'icehouse' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_gre(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', True) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_l3ha_on(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + def test_get_dvr_l2pop(self): + self.test_config.set('enable-dvr', True) + self.test_config.set('enable-l3ha', False) + self.test_config.set('overlay-network-type', 'vxlan') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + self.assertEquals(context.get_dvr(), False) + + class IdentityServiceContext(CharmTestCase): def setUp(self): @@ -152,6 +268,8 @@ class NeutronCCContextTest(CharmTestCase): plugin.return_value = None ctxt_data = { 'debug': True, + 'enable_dvr': False, + 'l3_ha': False, 'external_network': 'bob', 'neutron_bind_port': self.api_port, 'verbose': True, @@ -181,6 +299,8 @@ class NeutronCCContextTest(CharmTestCase): self.test_config.set('overlay-network-type', 'vxlan') ctxt_data = { 'debug': True, + 'enable_dvr': False, + 'l3_ha': False, 'external_network': 'bob', 'neutron_bind_port': self.api_port, 'verbose': True, @@ -202,6 +322,43 @@ 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') + @patch('__builtin__.__import__') + def test_neutroncc_context_l3ha(self, _import, plugin, nm): + plugin.return_value = None + self.test_config.set('enable-l3ha', True) + self.test_config.set('overlay-network-type', 'gre') + self.test_config.set('neutron-plugin', 'ovs') + self.test_config.set('l2-population', False) + self.os_release.return_value = 'juno' + ctxt_data = { + 'debug': True, + 'enable_dvr': False, + 'l3_ha': True, + 'external_network': 'bob', + 'neutron_bind_port': self.api_port, + 'verbose': True, + 'l2_population': False, + 'overlay_network_type': 'gre', + 'max_l3_agents_per_router': 2, + 'min_l3_agents_per_router': 2, + 'quota_floatingip': 50, + 'quota_health_monitors': -1, + 'quota_member': -1, + 'quota_network': 10, + 'quota_pool': 10, + 'quota_port': 50, + 'quota_router': 10, + 'quota_security_group': 10, + 'quota_security_group_rule': 100, + 'quota_subnet': 10, + 'quota_vip': 10, + } + napi_ctxt = context.NeutronCCContext() + with patch.object(napi_ctxt, '_ensure_packages'): + self.assertEquals(ctxt_data, napi_ctxt()) + @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('__builtin__.__import__') diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index 5bf61cf4..ee69e861 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -32,8 +32,12 @@ TO_PATCH = [ 'determine_packages', 'determine_ports', 'do_openstack_upgrade', + 'dvr_router_present', + 'l3ha_router_present', 'execd_preinstall', 'filter_installed_packages', + 'get_dvr', + 'get_l3ha', 'get_l2population', 'get_overlay_network_type', 'is_relation_made', @@ -93,6 +97,8 @@ class NeutronAPIHooksTests(CharmTestCase): @patch.object(hooks, 'configure_https') def test_config_changed(self, conf_https): self.openstack_upgrade_available.return_value = True + self.dvr_router_present.return_value = False + self.l3ha_router_present.return_value = False self.relation_ids.side_effect = self._fake_relids _n_api_rel_joined = self.patch('neutron_api_relation_joined') _n_plugin_api_rel_joined =\ @@ -274,9 +280,49 @@ class NeutronAPIHooksTests(CharmTestCase): def test_neutron_plugin_api_relation_joined_nol2(self): _relation_data = { 'neutron-security-groups': False, + 'enable-dvr': False, + 'enable-l3ha': False, 'l2-population': False, 'overlay-network-type': 'vxlan', } + 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._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_dvr(self): + _relation_data = { + 'neutron-security-groups': False, + 'enable-dvr': True, + 'enable-l3ha': False, + 'l2-population': True, + 'overlay-network-type': 'vxlan', + } + self.get_dvr.return_value = True + self.get_l3ha.return_value = False + self.get_l2population.return_value = True + self.get_overlay_network_type.return_value = 'vxlan' + 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_l3ha(self): + _relation_data = { + 'neutron-security-groups': False, + 'enable-dvr': False, + 'enable-l3ha': True, + 'l2-population': False, + 'overlay-network-type': 'vxlan', + } + self.get_dvr.return_value = False + self.get_l3ha.return_value = True self.get_l2population.return_value = False self.get_overlay_network_type.return_value = 'vxlan' self._call_hook('neutron-plugin-api-relation-joined') @@ -292,7 +338,11 @@ class NeutronAPIHooksTests(CharmTestCase): 'l2-population': False, 'overlay-network-type': 'vxlan', 'network-device-mtu': 1500, + 'enable-l3ha': True, + 'enable-dvr': True, } + self.get_dvr.return_value = True + self.get_l3ha.return_value = True self.get_l2population.return_value = False self.get_overlay_network_type.return_value = 'vxlan' self._call_hook('neutron-plugin-api-relation-joined')