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
This commit is contained in:
Frode Nordahl 2018-10-26 15:14:51 +02:00
parent 31a7dd32d7
commit d58ec757d5
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
8 changed files with 297 additions and 20 deletions

View File

@ -0,0 +1 @@
neutron_api_hooks.py

View File

@ -0,0 +1 @@
neutron_api_hooks.py

View File

@ -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'):

View File

@ -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':

View File

@ -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

View File

@ -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" %}

View File

@ -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()()

View File

@ -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 = \