diff --git a/config.yaml b/config.yaml index 13c9511f..d2f53d1f 100644 --- a/config.yaml +++ b/config.yaml @@ -368,3 +368,9 @@ options: description: | A comma-separated list of nagios servicegroups. If left empty, the nagios_context will be used as the servicegroup + manage-neutron-plugin-legacy-mode: + type: boolean + default: True + description: | + If True neutron-server will install neutron packages for the plugin + configured. diff --git a/hooks/neutron-plugin-api-subordinate-relation-changed b/hooks/neutron-plugin-api-subordinate-relation-changed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/neutron-plugin-api-subordinate-relation-changed @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-subordinate-relation-departed b/hooks/neutron-plugin-api-subordinate-relation-departed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/neutron-plugin-api-subordinate-relation-departed @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-subordinate-relation-joined b/hooks/neutron-plugin-api-subordinate-relation-joined new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/neutron-plugin-api-subordinate-relation-joined @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/neutron_api_context.py b/hooks/neutron_api_context.py index c2cd990f..a7e60af3 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -234,3 +234,63 @@ class HAProxyContext(context.HAProxyContext): # for haproxy.conf ctxt['service_ports'] = port_mapping return ctxt + + +class NeutronApiSDNContext(context.SubordinateConfigContext): + interfaces = 'neutron-plugin-api-subordinate' + + def __init__(self): + super(NeutronApiSDNContext, self).__init__( + interface='neutron-plugin-api-subordinate', + service='neutron-api', + config_file='/etc/neutron/neutron.conf') + + def __call__(self): + ctxt = super(NeutronApiSDNContext, self).__call__() + defaults = { + 'core-plugin': { + 'templ_key': 'core_plugin', + 'value': 'neutron.plugins.ml2.plugin.Ml2Plugin', + }, + 'neutron-plugin-config': { + 'templ_key': 'neutron_plugin_config', + 'value': '/etc/neutron/plugins/ml2/ml2_conf.ini', + }, + 'service-plugins': { + 'templ_key': 'service_plugins', + 'value': 'router,firewall,lbaas,vpnaas,metering', + }, + 'restart-trigger': { + 'templ_key': 'restart_trigger', + 'value': '', + }, + } + for rid in relation_ids('neutron-plugin-api-subordinate'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + plugin = rdata.get('neutron-plugin') + if not plugin: + continue + ctxt['neutron_plugin'] = plugin + for key in defaults.keys(): + remote_value = rdata.get(key) + ctxt_key = defaults[key]['templ_key'] + if remote_value: + ctxt[ctxt_key] = remote_value + else: + ctxt[ctxt_key] = defaults[key]['value'] + return ctxt + return ctxt + + +class NeutronApiSDNConfigFileContext(context.OSContextGenerator): + interfaces = ['neutron-plugin-api-subordinate'] + + def __call__(self): + for rid in relation_ids('neutron-plugin-api-subordinate'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + neutron_server_plugin_conf = rdata.get('neutron-plugin-config') + if neutron_server_plugin_conf: + return {'config': neutron_server_plugin_conf} + return {'config': '/etc/neutron/plugins/ml2/ml2_conf.ini'} diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index fde9e188..b79571e1 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -479,7 +479,8 @@ def zeromq_configuration_relation_joined(relid=None): users="neutron") -@hooks.hook('zeromq-configuration-relation-changed') +@hooks.hook('zeromq-configuration-relation-changed', + 'neutron-plugin-api-subordinate-relation-changed') @restart_on_change(restart_map(), stopstart=True) def zeromq_configuration_relation_changed(): CONFIGS.write_all() diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index 89869bdd..a7bda492 100644 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -160,16 +160,21 @@ def api_port(service): return API_PORTS[service] +def manage_plugin(): + return config('manage-neutron-plugin-legacy-mode') + + def determine_packages(source=None): # currently all packages match service names packages = [] + BASE_PACKAGES for v in resource_map().values(): packages.extend(v['services']) - pkgs = neutron_plugin_attribute(config('neutron-plugin'), - 'server_packages', - 'neutron') - packages.extend(pkgs) + if manage_plugin(): + pkgs = neutron_plugin_attribute(config('neutron-plugin'), + 'server_packages', + 'neutron') + packages.extend(pkgs) if get_os_codename_install_source(source) >= 'kilo': packages.extend(KILO_PACKAGES) @@ -211,24 +216,31 @@ def resource_map(): else: resource_map.pop(APACHE_24_CONF) - # add neutron plugin requirements. nova-c-c only needs the neutron-server - # associated with configs, not the plugin agent. - plugin = config('neutron-plugin') - conf = neutron_plugin_attribute(plugin, 'config', 'neutron') - ctxts = (neutron_plugin_attribute(plugin, 'contexts', 'neutron') - or []) - services = neutron_plugin_attribute(plugin, 'server_services', - 'neutron') - resource_map[conf] = {} - resource_map[conf]['services'] = services - resource_map[conf]['contexts'] = ctxts - resource_map[conf]['contexts'].append( - neutron_api_context.NeutronCCContext()) + if manage_plugin(): + # add neutron plugin requirements. nova-c-c only needs the + # neutron-server associated with configs, not the plugin agent. + plugin = config('neutron-plugin') + conf = neutron_plugin_attribute(plugin, 'config', 'neutron') + ctxts = (neutron_plugin_attribute(plugin, 'contexts', 'neutron') + or []) + services = neutron_plugin_attribute(plugin, 'server_services', + 'neutron') + resource_map[conf] = {} + resource_map[conf]['services'] = services + resource_map[conf]['contexts'] = ctxts + resource_map[conf]['contexts'].append( + neutron_api_context.NeutronCCContext()) - # update for postgres - resource_map[conf]['contexts'].append( - context.PostgresqlDBContext(database=config('database'))) + # update for postgres + resource_map[conf]['contexts'].append( + context.PostgresqlDBContext(database=config('database'))) + else: + resource_map[NEUTRON_CONF]['contexts'].append( + neutron_api_context.NeutronApiSDNContext() + ) + resource_map[NEUTRON_DEFAULT]['contexts'] = \ + [neutron_api_context.NeutronApiSDNConfigFileContext()] return resource_map diff --git a/metadata.yaml b/metadata.yaml index 0c8256be..3ef46991 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -37,6 +37,9 @@ requires: zeromq-configuration: interface: zeromq-configuration scope: container + neutron-plugin-api-subordinate: + interface: neutron-plugin-api-subordinate + scope: container peers: cluster: interface: neutron-api-ha diff --git a/templates/icehouse/neutron.conf b/templates/icehouse/neutron.conf index c78169d5..177eaea3 100644 --- a/templates/icehouse/neutron.conf +++ b/templates/icehouse/neutron.conf @@ -27,10 +27,14 @@ bind_port = 9696 {% if core_plugin -%} core_plugin = {{ core_plugin }} +{% if service_plugins -%} +service_plugins = {{ service_plugins }} +{% else -%} {% 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 -%} +{% endif -%} {% if neutron_security_groups -%} allow_overlapping_ips = True @@ -50,6 +54,12 @@ nova_admin_password = {{ admin_password }} nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 {% endif -%} +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + [quotas] quota_driver = neutron.db.quota_db.DbQuotaDriver {% if neutron_security_groups -%} diff --git a/templates/juno/neutron.conf b/templates/juno/neutron.conf index 05d3e212..a4c97285 100644 --- a/templates/juno/neutron.conf +++ b/templates/juno/neutron.conf @@ -54,6 +54,12 @@ nova_admin_password = {{ admin_password }} nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 {% endif -%} +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + [quotas] quota_driver = neutron.db.quota_db.DbQuotaDriver {% if neutron_security_groups -%} diff --git a/templates/kilo/neutron.conf b/templates/kilo/neutron.conf index a6a3c664..07a0ed88 100644 --- a/templates/kilo/neutron.conf +++ b/templates/kilo/neutron.conf @@ -31,10 +31,14 @@ bind_port = 9696 {% if core_plugin -%} core_plugin = {{ core_plugin }} +{% if service_plugins -%} +service_plugins = {{ service_plugins }} +{% else -%} {% if neutron_plugin in ['ovs', 'ml2'] -%} service_plugins = router,firewall,lbaas,vpnaas,metering {% endif -%} {% endif -%} +{% endif -%} {% if neutron_security_groups -%} allow_overlapping_ips = True @@ -52,6 +56,12 @@ nova_admin_password = {{ admin_password }} nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 {% endif -%} +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + {% include "section-zeromq" %} [quotas] diff --git a/unit_tests/test_neutron_api_context.py b/unit_tests/test_neutron_api_context.py index 11edcffa..cf4dd6b2 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -1,3 +1,4 @@ +import json from test_utils import CharmTestCase from mock import patch import neutron_api_context as context @@ -432,3 +433,135 @@ class NeutronCCContextTest(CharmTestCase): } for key in expect.iterkeys(): self.assertEquals(napi_ctxt[key], expect[key]) + + +class NeutronApiSDNContextTest(CharmTestCase): + + def setUp(self): + super(NeutronApiSDNContextTest, self).setUp(context, TO_PATCH) + self.relation_get.side_effect = self.test_relation.get + + def tearDown(self): + super(NeutronApiSDNContextTest, self).tearDown() + + def test_init(self): + napisdn_ctxt = context.NeutronApiSDNContext() + self.assertEquals( + napisdn_ctxt.interfaces, + ['neutron-plugin-api-subordinate'] + ) + self.assertEquals(napisdn_ctxt.services, ['neutron-api']) + self.assertEquals( + napisdn_ctxt.config_file, + '/etc/neutron/neutron.conf' + ) + + @patch.object(charmhelpers.contrib.openstack.context, 'log') + @patch.object(charmhelpers.contrib.openstack.context, 'relation_get') + @patch.object(charmhelpers.contrib.openstack.context, 'related_units') + @patch.object(charmhelpers.contrib.openstack.context, 'relation_ids') + def ctxt_check(self, rel_settings, expect, _rids, _runits, _rget, _log): + self.test_relation.set(rel_settings) + _runits.return_value = ['unit1'] + _rids.return_value = ['rid2'] + _rget.side_effect = self.test_relation.get + self.relation_ids.return_value = ['rid2'] + self.related_units.return_value = ['unit1'] + napisdn_ctxt = context.NeutronApiSDNContext()() + self.assertEquals(napisdn_ctxt, expect) + + def test_defaults(self): + self.ctxt_check( + {'neutron-plugin': 'ovs'}, + { + 'core_plugin': 'neutron.plugins.ml2.plugin.Ml2Plugin', + 'neutron_plugin_config': ('/etc/neutron/plugins/ml2/' + 'ml2_conf.ini'), + 'service_plugins': 'router,firewall,lbaas,vpnaas,metering', + 'restart_trigger': '', + 'neutron_plugin': 'ovs', + 'sections': {}, + } + ) + + def test_overrides(self): + self.ctxt_check( + { + 'neutron-plugin': 'ovs', + 'core-plugin': 'neutron.plugins.ml2.plugin.MidoPlumODL', + 'neutron-plugin-config': '/etc/neutron/plugins/fl/flump.ini', + 'service-plugins': 'router,unicorn,rainbows', + 'restart-trigger': 'restartnow', + }, + { + 'core_plugin': 'neutron.plugins.ml2.plugin.MidoPlumODL', + 'neutron_plugin_config': '/etc/neutron/plugins/fl/flump.ini', + 'service_plugins': 'router,unicorn,rainbows', + 'restart_trigger': 'restartnow', + 'neutron_plugin': 'ovs', + 'sections': {}, + } + ) + + def test_subordinateconfig(self): + principle_config = { + "neutron-api": { + "/etc/neutron/neutron.conf": { + "sections": { + 'DEFAULT': [ + ('neutronboost', True) + ], + } + } + } + } + self.ctxt_check( + { + 'neutron-plugin': 'ovs', + 'subordinate_configuration': json.dumps(principle_config), + }, + { + 'core_plugin': 'neutron.plugins.ml2.plugin.Ml2Plugin', + 'neutron_plugin_config': ('/etc/neutron/plugins/ml2/' + 'ml2_conf.ini'), + 'service_plugins': 'router,firewall,lbaas,vpnaas,metering', + 'restart_trigger': '', + 'neutron_plugin': 'ovs', + 'sections': {u'DEFAULT': [[u'neutronboost', True]]}, + } + ) + + def test_empty(self): + self.ctxt_check( + {}, + {'sections': {}}, + ) + + +class NeutronApiSDNConfigFileContextTest(CharmTestCase): + + def setUp(self): + super(NeutronApiSDNConfigFileContextTest, self).setUp( + context, TO_PATCH) + self.relation_get.side_effect = self.test_relation.get + + def tearDown(self): + super(NeutronApiSDNConfigFileContextTest, self).tearDown() + + def test_configset(self): + self.test_relation.set({ + 'neutron-plugin-config': '/etc/neutron/superplugin.ini' + }) + self.relation_ids.return_value = ['rid2'] + self.related_units.return_value = ['unit1'] + napisdn_ctxt = context.NeutronApiSDNConfigFileContext()() + self.assertEquals(napisdn_ctxt, { + 'config': '/etc/neutron/superplugin.ini' + }) + + def test_default(self): + self.relation_ids.return_value = [] + napisdn_ctxt = context.NeutronApiSDNConfigFileContext()() + self.assertEquals(napisdn_ctxt, { + 'config': '/etc/neutron/plugins/ml2/ml2_conf.ini' + }) diff --git a/unit_tests/test_neutron_api_utils.py b/unit_tests/test_neutron_api_utils.py index f92bf27c..d6160d22 100644 --- a/unit_tests/test_neutron_api_utils.py +++ b/unit_tests/test_neutron_api_utils.py @@ -104,28 +104,57 @@ class TestNeutronAPIUtils(CharmTestCase): expect.extend(nutils.KILO_PACKAGES) self.assertItemsEqual(pkg_list, expect) + @patch.object(nutils, 'git_install_requested') + def test_determine_packages_noplugin(self, git_requested): + git_requested.return_value = False + self.test_config.set('manage-neutron-plugin-legacy-mode', False) + pkg_list = nutils.determine_packages() + expect = deepcopy(nutils.BASE_PACKAGES) + expect.extend(['neutron-server']) + self.assertItemsEqual(pkg_list, expect) + def test_determine_ports(self): port_list = nutils.determine_ports() self.assertItemsEqual(port_list, [9696]) + @patch.object(nutils, 'manage_plugin') @patch('os.path.exists') - def test_resource_map(self, _path_exists): + def test_resource_map(self, _path_exists, _manage_plugin): _path_exists.return_value = False + _manage_plugin.return_value = True _map = nutils.resource_map() confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT, nutils.APACHE_CONF] [self.assertIn(q_conf, _map.keys()) for q_conf in confs] self.assertTrue(nutils.APACHE_24_CONF not in _map.keys()) + @patch.object(nutils, 'manage_plugin') @patch('os.path.exists') - def test_resource_map_apache24(self, _path_exists): + def test_resource_map_apache24(self, _path_exists, _manage_plugin): _path_exists.return_value = True + _manage_plugin.return_value = True _map = nutils.resource_map() confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT, nutils.APACHE_24_CONF] [self.assertIn(q_conf, _map.keys()) for q_conf in confs] self.assertTrue(nutils.APACHE_CONF not in _map.keys()) + @patch.object(nutils, 'manage_plugin') + @patch('os.path.exists') + def test_resource_map_noplugin(self, _path_exists, _manage_plugin): + _path_exists.return_value = True + _manage_plugin.return_value = False + _map = nutils.resource_map() + found_sdn_ctxt = False + found_sdnconfig_ctxt = False + for ctxt in _map[nutils.NEUTRON_CONF]['contexts']: + if isinstance(ctxt, ncontext.NeutronApiSDNContext): + found_sdn_ctxt = True + for ctxt in _map[nutils.NEUTRON_DEFAULT]['contexts']: + if isinstance(ctxt, ncontext.NeutronApiSDNConfigFileContext): + found_sdnconfig_ctxt = True + self.assertTrue(found_sdn_ctxt and found_sdnconfig_ctxt) + @patch('os.path.exists') def test_restart_map(self, mock_path_exists): mock_path_exists.return_value = False @@ -520,3 +549,13 @@ class TestNeutronAPIUtils(CharmTestCase): 'upgrade', 'head'] self.subprocess.check_output.assert_called_with(cmd) + + def test_manage_plugin_true(self): + self.test_config.set('manage-neutron-plugin-legacy-mode', True) + manage = nutils.manage_plugin() + self.assertTrue(manage) + + def test_manage_plugin_false(self): + self.test_config.set('manage-neutron-plugin-legacy-mode', False) + manage = nutils.manage_plugin() + self.assertFalse(manage)