From d58ec757d51eb3644763a601268ba05e35993651 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Fri, 26 Oct 2018 15:14:51 +0200 Subject: [PATCH] Add neutron-load-balancer interface The Neutron built-in LBaaS provider is deprecated as of OpenStack version Queens and the service is to be replaced by a separate service such as Octavia. This interface serves the purpose of notifying a external load balancer service of when the Neutron API is ready to accept queries. In a transition period it is also used by the ``neutron-api`` charm to determine whether it should configure Neutron with the legacy LBaaS provider enabled or if it should enable the ``lbaasv2-proxy`` driver to proxy load balancer requests sent to the Neutron API to the external service. Change-Id: Id9f7ffb3d363c7606d92af592b9803644046d865 --- hooks/neutron-load-balancer-relation-changed | 1 + hooks/neutron-load-balancer-relation-joined | 1 + hooks/neutron_api_context.py | 44 ++++++- hooks/neutron_api_hooks.py | 14 +++ metadata.yaml | 2 + templates/rocky/neutron.conf | 126 +++++++++++++++++++ unit_tests/test_neutron_api_context.py | 110 +++++++++++++--- unit_tests/test_neutron_api_hooks.py | 19 +++ 8 files changed, 297 insertions(+), 20 deletions(-) create mode 120000 hooks/neutron-load-balancer-relation-changed create mode 120000 hooks/neutron-load-balancer-relation-joined create mode 100644 templates/rocky/neutron.conf diff --git a/hooks/neutron-load-balancer-relation-changed b/hooks/neutron-load-balancer-relation-changed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/neutron-load-balancer-relation-changed @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/neutron-load-balancer-relation-joined b/hooks/neutron-load-balancer-relation-joined new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/neutron-load-balancer-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 c55973c0..33558fea 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -13,7 +13,9 @@ # limitations under the License. import ast +import json import re +import traceback from collections import OrderedDict @@ -499,6 +501,10 @@ class NeutronCCContext(context.NeutronContext): ctxt['mechanism_drivers'] = get_ml2_mechanism_drivers() + n_load_balancer_settings = NeutronLoadBalancerContext()() + if n_load_balancer_settings: + ctxt.update(n_load_balancer_settings) + if config('neutron-plugin') in ['ovs', 'ml2', 'Calico']: ctxt['service_plugins'] = [] service_plugins = { @@ -536,10 +542,22 @@ class NeutronCCContext(context.NeutronContext): 'LoadBalancerPluginv2'), ('neutron_dynamic_routing.' 'services.bgp.bgp_plugin.BgpPlugin')], + 'rocky': ['router', 'firewall', 'metering', 'segments', + ('neutron_dynamic_routing.' + 'services.bgp.bgp_plugin.BgpPlugin')], } + if cmp_release >= 'rocky': + if ctxt.get('load_balancer_name', None): + # TODO(fnordahl): Remove when ``neutron_lbaas`` is retired + service_plugins['rocky'].append('lbaasv2-proxy') + else: + # TODO(fnordahl): Remove fall-back in next charm release + service_plugins['rocky'].append( + 'neutron_lbaas.services.loadbalancer.plugin.' + 'LoadBalancerPluginv2') ctxt['service_plugins'] = service_plugins.get( - release, service_plugins['pike']) + release, service_plugins['rocky']) if is_nsg_logging_enabled(): ctxt['service_plugins'].append('log') @@ -756,6 +774,30 @@ class NeutronApiApiPasteContext(context.OSContextGenerator): if extra_middleware else {} +class NeutronLoadBalancerContext(context.OSContextGenerator): + interfaces = ['neutron-load-balancer'] + + def __call__(self): + ctxt = {} + for rid in relation_ids('neutron-load-balancer'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + try: + ctxt['load_balancer_name'] = json.loads( + rdata.get('name')) + ctxt['load_balancer_base_url'] = json.loads( + rdata.get('base_url')) + except TypeError: + pass + except json.decoder.JSONDecodeError: + log(traceback.format_exc()) + raise ValueError('Invalid load balancer data' + ' - check the related charm') + if self.context_complete(ctxt): + return ctxt + return {} + + class MidonetContext(context.OSContextGenerator): def __init__(self, rel_name='midonet'): diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index 6e4c750f..3fcee0e3 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -448,6 +448,20 @@ def neutron_api_relation_changed(): CONFIGS.write(NEUTRON_CONF) +@hooks.hook('neutron-load-balancer-relation-joined') +def neutron_load_balancer_relation_joined(rid=None): + relation_data = {} + relation_data['neutron-api-ready'] = is_api_ready(CONFIGS) + relation_set(relation_id=rid, **relation_data) + + +@hooks.hook('neutron-load-balancer-relation-changed') +@restart_on_change(restart_map()) +def neutron_load_balancer_relation_changed(rid=None): + neutron_load_balancer_relation_joined(rid) + CONFIGS.write(NEUTRON_CONF) + + @hooks.hook('neutron-plugin-api-relation-joined') def neutron_plugin_api_relation_joined(rid=None): if config('neutron-plugin') == 'nsx': diff --git a/metadata.yaml b/metadata.yaml index c1e5756d..3f2287ab 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -31,6 +31,8 @@ provides: interface: neutron-api neutron-plugin-api: interface: neutron-plugin-api + neutron-load-balancer: + interface: neutron-load-balancer requires: shared-db: interface: mysql-shared diff --git a/templates/rocky/neutron.conf b/templates/rocky/neutron.conf new file mode 100644 index 00000000..83bd8b00 --- /dev/null +++ b/templates/rocky/neutron.conf @@ -0,0 +1,126 @@ +# rocky +############################################################################### +# [ 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 +bind_host = {{ bind_host }} +auth_strategy = keystone +api_workers = {{ workers }} +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 }} +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 service_plugins -%} +service_plugins = {{ service_plugins }} +{% endif -%} +{% endif -%} + +{% if neutron_security_groups -%} +allow_overlapping_ips = True +{% endif -%} + +dhcp_agents_per_network = {{ dhcp_agents_per_network }} + +notify_nova_on_port_status_changes = True +notify_nova_on_port_data_changes = True + +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + +{% if user_config_flags -%} +{% for key, value in user_config_flags.items() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +{% if global_physnet_mtu -%} +global_physnet_mtu = {{ global_physnet_mtu }} +{% endif -%} + +{% if enable_designate -%} +external_dns_driver = designate +{% endif -%} + +{% include "section-zeromq" %} + +[quotas] +{% if quota_driver -%} +quota_driver = {{ quota_driver }} +{% else -%} +quota_driver = neutron.db.quota.driver.DbQuotaDriver +{% endif -%} +{% if neutron_security_groups -%} +quota_items = network,subnet,port,security_group,security_group_rule +quota_security_group = {{ quota_security_group }} +quota_security_group_rule = {{ quota_security_group_rule }} +{% else -%} +quota_items = network,subnet,port +{% endif -%} +quota_network = {{ quota_network }} +quota_subnet = {{ quota_subnet }} +quota_port = {{ quota_port }} +quota_vip = {{ quota_vip }} +quota_pool = {{ quota_pool }} +quota_member = {{ quota_member }} +quota_health_monitors = {{ quota_health_monitors }} +quota_router = {{ quota_router }} +quota_floatingip = {{ quota_floatingip }} + +[agent] +root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf + +{% include "section-keystone-authtoken-mitaka" %} + +{% include "parts/section-database" %} + +{% include "section-rabbitmq-oslo" %} + +{% include "section-oslo-notifications" %} + +[oslo_concurrency] +lock_path = $state_path/lock + +{% include "parts/section-nova" %} + +{% if enable_designate -%} +{% include "parts/section-designate" %} +{% endif -%} + +{% include "parts/section-placement" %} + +{% if load_balancer_name -%} +[{{ load_balancer_name }}] +base_url = {{ load_balancer_base_url }} +{% else -%} +[service_providers] +service_provider = LOADBALANCERV2:Haproxy:neutron_lbaas.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default +{% endif %} + +{% include "section-oslo-middleware" %} diff --git a/unit_tests/test_neutron_api_context.py b/unit_tests/test_neutron_api_context.py index 21a966fb..d4e4ae51 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -14,7 +14,7 @@ import json -from mock import patch +from mock import MagicMock, patch import neutron_api_context as context import charmhelpers @@ -406,10 +406,11 @@ class NeutronCCContextTest(CharmTestCase): def tearDown(self): super(NeutronCCContextTest, self).tearDown() + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_no_setting(self, _import, plugin, nm): + def test_neutroncc_context_no_setting(self, _import, plugin, nm, nlb): plugin.return_value = None ctxt_data = { 'debug': True, @@ -450,10 +451,12 @@ class NeutronCCContextTest(CharmTestCase): with patch.object(napi_ctxt, '_ensure_packages'): self.assertEqual(ctxt_data, napi_ctxt()) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_no_setting_mitaka(self, _import, plugin, nm): + def test_neutroncc_context_no_setting_mitaka(self, _import, plugin, nm, + nlb): plugin.return_value = None ctxt_data = { 'debug': True, @@ -492,9 +495,10 @@ class NeutronCCContextTest(CharmTestCase): with patch.object(napi_ctxt, '_ensure_packages'): self.assertEqual(ctxt_data, napi_ctxt()) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') - def test_neutroncc_context_dns_setting(self, plugin, nm): + def test_neutroncc_context_dns_setting(self, plugin, nm, nlb): plugin.return_value = None self.test_config.set('enable-ml2-dns', True) self.test_config.set('dns-domain', 'example.org.') @@ -505,10 +509,11 @@ class NeutronCCContextTest(CharmTestCase): self.assertEqual('example.org.', ctxt['dns_domain']) self.assertEqual('port_security,dns', ctxt['extension_drivers']) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') def test_neutroncc_context_dns_no_port_security_setting(self, - plugin, nm): + plugin, nm, nlb): """Verify extension drivers without port security.""" plugin.return_value = None self.test_config.set('enable-ml2-port-security', False) @@ -521,9 +526,10 @@ class NeutronCCContextTest(CharmTestCase): self.assertEqual('example.org.', ctxt['dns_domain']) self.assertEqual('dns', ctxt['extension_drivers']) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') - def test_neutroncc_context_dns_kilo(self, plugin, nm): + def test_neutroncc_context_dns_kilo(self, plugin, nm, nlb): """Verify dns extension and domain are not specified in kilo.""" plugin.return_value = None self.test_config.set('enable-ml2-port-security', False) @@ -536,10 +542,11 @@ class NeutronCCContextTest(CharmTestCase): self.assertFalse('dns_domain' in ctxt) self.assertFalse('extension_drivers' in ctxt) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_vxlan(self, _import, plugin, nm): + def test_neutroncc_context_vxlan(self, _import, plugin, nm, nlb): plugin.return_value = None self.test_config.set('flat-network-providers', 'physnet2 physnet3') self.test_config.set('overlay-network-type', 'vxlan') @@ -584,10 +591,11 @@ class NeutronCCContextTest(CharmTestCase): with patch.object(napi_ctxt, '_ensure_packages'): self.assertEqual(ctxt_data, napi_ctxt()) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_l3ha(self, _import, plugin, nm): + def test_neutroncc_context_l3ha(self, _import, plugin, nm, nlb): plugin.return_value = None self.test_config.set('enable-l3ha', True) self.test_config.set('enable-qos', False) @@ -651,10 +659,11 @@ class NeutronCCContextTest(CharmTestCase): with patch.object(napi_ctxt, '_ensure_packages'): self.assertRaises(ValueError, napi_ctxt) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_sriov(self, _import, plugin, nm): + def test_neutroncc_context_sriov(self, _import, plugin, nm, nlb): plugin.return_value = None self.test_config.set('enable-sriov', True) self.test_config.set('supported-pci-vendor-devs', @@ -704,10 +713,11 @@ class NeutronCCContextTest(CharmTestCase): with self.assertRaises(Exception) as context: context.NeutronCCContext() + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_api_rel(self, _import, plugin, nm): + def test_neutroncc_context_api_rel(self, _import, plugin, nm, nlb): nova_url = 'http://127.0.0.10' plugin.return_value = None self.os_release.return_value = 'havana' @@ -732,10 +742,11 @@ class NeutronCCContextTest(CharmTestCase): napi_ctxt._ensure_packages() ep.assert_has_calls([]) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_nsx(self, _import, plugin, nm): + def test_neutroncc_context_nsx(self, _import, plugin, nm, nlb): plugin.return_value = 'nsx' self.os_release.return_value = 'havana' self.related_units.return_value = [] @@ -752,10 +763,11 @@ class NeutronCCContextTest(CharmTestCase): for key in expect.keys(): self.assertEqual(napi_ctxt[key], expect[key]) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_nuage(self, _import, plugin, nm): + def test_neutroncc_context_nuage(self, _import, plugin, nm, nlb): plugin.return_value = 'vsp' self.os_release.return_value = 'havana' self.related_units.return_value = ['vsdunit1'] @@ -772,10 +784,11 @@ class NeutronCCContextTest(CharmTestCase): for key in expect.keys(): self.assertEqual(napi_ctxt[key], expect[key]) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_qos(self, _import, plugin, nm): + def test_neutroncc_context_qos(self, _import, plugin, nm, nlb): plugin.return_value = None self.os_release.return_value = 'mitaka' self.test_config.set('enable-qos', True) @@ -789,10 +802,11 @@ class NeutronCCContextTest(CharmTestCase): for key in expect.keys(): self.assertEqual(napi_ctxt[key], expect[key]) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_vlan_trunking(self, _import, plugin, nm): + def test_neutroncc_context_vlan_trunking(self, _import, plugin, nm, nlb): plugin.return_value = None self.os_release.return_value = 'newton' self.test_config.set('neutron-plugin', 'ovs') @@ -804,11 +818,12 @@ class NeutronCCContextTest(CharmTestCase): self.assertEqual(napi_ctxt['service_plugins'], expected_service_plugins) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') def test_neutroncc_context_vlan_trunking_invalid_plugin(self, _import, - plugin, nm): + plugin, nm, nlb): plugin.return_value = None self.os_release.return_value = 'newton' self.test_config.set('neutron-plugin', 'Calico') @@ -820,11 +835,12 @@ class NeutronCCContextTest(CharmTestCase): self.assertEqual(napi_ctxt['service_plugins'], expected_service_plugins) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') def test_neutroncc_context_vlan_trunking_invalid_release(self, _import, - plugin, nm): + plugin, nm, nlb): plugin.return_value = None self.os_release.return_value = 'mitaka' self.test_config.set('neutron-plugin', 'ovs') @@ -834,10 +850,11 @@ class NeutronCCContextTest(CharmTestCase): self.assertEqual(napi_ctxt['service_plugins'], expected_service_plugins) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') @patch('builtins.__import__') - def test_neutroncc_context_service_plugins(self, _import, plugin, nm): + def test_neutroncc_context_service_plugins(self, _import, plugin, nm, nlb): plugin.return_value = None self.test_config.set('enable-qos', False) self.test_config.set('enable-ml2-port-security', False) @@ -902,10 +919,30 @@ class NeutronCCContextTest(CharmTestCase): 'neutron_dynamic_routing.services.bgp.bgp_plugin.BgpPlugin') self.assertEqual(context.NeutronCCContext()()['service_plugins'], service_plugins) + # rocky + self.os_release.return_value = 'rocky' + service_plugins = ( + 'router,firewall,metering,segments,' + 'neutron_dynamic_routing.services.bgp.bgp_plugin.BgpPlugin,' + 'neutron_lbaas.services.loadbalancer.plugin.LoadBalancerPluginv2') + self.assertEqual(context.NeutronCCContext()()['service_plugins'], + service_plugins) + # rocky and related to charm through neutron-load-balancer interface + self.os_release.return_value = 'rocky' + service_plugins = ( + 'router,firewall,metering,segments,' + 'neutron_dynamic_routing.services.bgp.bgp_plugin.BgpPlugin,' + 'lbaasv2-proxy') + lb_context = MagicMock() + lb_context.return_value = {'load_balancer_name': 'octavia'} + nlb.return_value = lb_context + self.assertEqual(context.NeutronCCContext()()['service_plugins'], + service_plugins) + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') - def test_neutroncc_context_physical_network_mtus(self, plugin, nm): + def test_neutroncc_context_physical_network_mtus(self, plugin, nm, nlb): plugin.return_value = None self.test_config.set('physical-network-mtus', 'provider1:4000') self.os_release.return_value = 'mitaka' @@ -914,9 +951,11 @@ class NeutronCCContextTest(CharmTestCase): ctxt = napi_ctxt() self.assertEqual(ctxt['physical_network_mtus'], 'provider1:4000') + @patch.object(context, 'NeutronLoadBalancerContext') @patch.object(context.NeutronCCContext, 'network_manager') @patch.object(context.NeutronCCContext, 'plugin') - def test_neutroncc_context_physical_network_mtus_multi(self, plugin, nm): + def test_neutroncc_context_physical_network_mtus_multi(self, plugin, nm, + nlb): plugin.return_value = None self.test_config.set('physical-network-mtus', 'provider1:4000 provider2:5000') @@ -1280,3 +1319,36 @@ class DesignateContextTest(CharmTestCase): 'ipv6_ptr_zone_prefix_size': 64} self.assertEqual(expect, ctxt) + + +class NeutronLoadBalancerContextTest(CharmTestCase): + + def setUp(self): + super(NeutronLoadBalancerContextTest, self).setUp(context, TO_PATCH) + self.relation_get.side_effect = self.test_relation.get + + def tearDown(self): + super(NeutronLoadBalancerContextTest, self).tearDown() + + def test_neutron_load_balancer_context(self): + expect = {} + ctxt = context.NeutronLoadBalancerContext()() + self.assertEqual(expect, ctxt) + self.related_units.return_value = ['unit1'] + self.relation_ids.return_value = ['rid1'] + expect = {'load_balancer_name': 'FAKENAME', + 'load_balancer_base_url': 'http://1.2.3.4:1234'} + self.test_relation.set({'name': json.dumps('FAKENAME'), + 'base_url': json.dumps('http://1.2.3.4:1234')}) + ctxt = context.NeutronLoadBalancerContext()() + self.assertEqual(expect, ctxt) + expect = {} + self.test_relation.set({'name': None, + 'base_url': 'http://1.2.3.4:1234'}) + ctxt = context.NeutronLoadBalancerContext()() + self.assertEqual(expect, ctxt) + expect = {} + self.test_relation.set({'name': 'FAKENAME', + 'base_url': 'http://1.2.3.4:1234'}) + with self.assertRaises(ValueError): + context.NeutronLoadBalancerContext()() diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index dd995cad..5a8a7dee 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -463,6 +463,25 @@ class NeutronAPIHooksTests(CharmTestCase): self._call_hook('neutron-api-relation-changed') self.assertTrue(self.CONFIGS.write.called_with(NEUTRON_CONF)) + @patch.object(hooks, 'is_api_ready') + def test_neutron_load_balancer_relation_joined(self, is_api_ready): + is_api_ready.return_value = True + self._call_hook('neutron-load-balancer-relation-joined') + is_api_ready.assert_called_once_with(self.CONFIGS) + _relation_data = {'neutron-api-ready': True} + self.relation_set.assert_called_once_with(relation_id=None, + **_relation_data) + + @patch.object(hooks, 'is_api_ready') + def test_neutron_load_balancer_relation_changed(self, is_api_ready): + is_api_ready.return_value = True + self._call_hook('neutron-load-balancer-relation-changed') + is_api_ready.assert_called_once_with(self.CONFIGS) + _relation_data = {'neutron-api-ready': True} + self.relation_set.assert_called_once_with(relation_id=None, + **_relation_data) + self.CONFIGS.write.assert_called_once_with(NEUTRON_CONF) + def test_neutron_plugin_api_relation_joined_nol2(self): self.unit_get.return_value = '172.18.18.18' self.IdentityServiceContext.return_value = \