Add support for Infoblox IPAM configuration via subordinate charm.

This change adds infoblox-api relation which allows neutron-server
to publish events to a remote infoblox server. Additionally this
change enables IPAM for the neutron service, which forces neutron
to authorize any network changes against the target Infoblox
server.

This change adds the proper hooks, context, and templates to add
infobox configuration to /etc/neutron/neutron.conf, passed by the
infoblox subordinate charm.

Closes-Bug: 1776689

Change-Id: Ib11377bd61c2b3fed5104ba0a423073a15cc18a2
This commit is contained in:
Michael Skalka 2018-06-06 12:30:56 -04:00 committed by Aymen Frikha
parent 123f366fc5
commit 92a1062830
16 changed files with 226 additions and 4 deletions

View File

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

View File

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

View File

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

View File

@ -879,3 +879,43 @@ class DesignateContext(context.OSContextGenerator):
ctxt['ipv6_ptr_zone_prefix_size'] = (
config('ipv6-ptr-zone-prefix-size'))
return ctxt
class NeutronInfobloxContext(context.OSContextGenerator):
'''Infoblox IPAM context for Neutron API'''
interfaces = ['infoblox-neutron']
def __call__(self):
ctxt = {}
rdata = {}
for rid in relation_ids('infoblox-neutron'):
if related_units(rid) and not rdata:
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt['cloud_data_center_id'] = rdata.get('dc_id')
break
if ctxt.get('cloud_data_center_id') is not None:
if not self.check_requirements(rdata):
log('Missing Infoblox connection information, passing.')
return {}
ctxt['enable_infoblox'] = True
ctxt['cloud_data_center_id'] = rdata.get('dc_id')
ctxt['grid_master_host'] = rdata.get('grid_master_host')
ctxt['grid_master_name'] = rdata.get('grid_master_name')
ctxt['infoblox_admin_user_name'] = rdata.get('admin_user_name')
ctxt['infoblox_admin_password'] = rdata.get('admin_password')
# the next three values are non-critical and may accept defaults
ctxt['wapi_version'] = rdata.get('wapi_version', '2.3')
ctxt['wapi_max_results'] = rdata.get('wapi_max_results', '-50000')
ctxt['wapi_paging'] = rdata.get('wapi_paging', True)
return ctxt
def check_requirements(self, rdata):
required = [
'grid_master_name',
'grid_master_host',
'admin_user_name',
'admin_password',
]
return len(set(p for p, v in rdata.items() if v).
intersection(required)) == len(required)

View File

@ -35,6 +35,7 @@ from charmhelpers.core.hookenv import (
open_port,
unit_get,
related_units,
is_leader,
)
from charmhelpers.core.host import (
@ -86,6 +87,7 @@ from neutron_api_utils import (
pause_unit_helper,
resume_unit_helper,
remove_old_packages,
is_db_initialised,
)
from neutron_api_context import (
get_dns_domain,
@ -294,6 +296,7 @@ def config_changed():
packages_removed = remove_old_packages()
configure_https()
update_nrpe_config()
infoblox_changed()
CONFIGS.write_all()
if packages_removed and not is_unit_paused_set():
log("Package purge detected, restarting services", "INFO")
@ -359,7 +362,7 @@ def db_changed():
return
CONFIGS.write_all()
conditional_neutron_migration()
infoblox_changed()
for r_id in relation_ids('neutron-plugin-api-subordinate'):
neutron_plugin_api_subordinate_relation_joined(relid=r_id)
@ -415,6 +418,7 @@ def identity_changed():
for r_id in relation_ids('neutron-plugin-api-subordinate'):
neutron_plugin_api_subordinate_relation_joined(relid=r_id)
configure_https()
infoblox_changed()
@hooks.hook('neutron-api-relation-joined')
@ -655,6 +659,34 @@ def designate_changed():
CONFIGS.write_all()
@hooks.hook('infoblox-neutron-relation-changed')
@restart_on_change(restart_map)
def infoblox_changed():
# The neutron DB upgrade will add new tables to
# neutron db related to infoblox service.
# Please take a look to charm-infoblox docs.
if 'infoblox-neutron' not in CONFIGS.complete_contexts():
log('infoblox-neutron relation incomplete. Peer not ready?')
return
CONFIGS.write(NEUTRON_CONF)
if is_leader():
ready = False
if is_db_initialised() and neutron_ready():
migrate_neutron_database(upgrade=True)
ready = True
for rid in relation_ids('infoblox-neutron'):
relation_set(relation_id=rid, neutron_api_ready=ready)
@hooks.hook('infoblox-neutron-relation-departed',
'infoblox-neutron-relation-broken')
@restart_on_change(restart_map)
def infoblox_departed():
CONFIGS.write_all()
@hooks.hook('update-status')
@harden()
@harden()

View File

@ -173,7 +173,8 @@ BASE_RESOURCE_MAP = OrderedDict([
context.WorkerConfigContext(),
context.InternalEndpointContext(),
context.MemcacheContext(),
neutron_api_context.DesignateContext()],
neutron_api_context.DesignateContext(),
neutron_api_context.NeutronInfobloxContext()],
}),
(NEUTRON_DEFAULT, {
'services': ['neutron-server'],

View File

@ -55,6 +55,9 @@ requires:
interface: midonet
external-dns:
interface: designate
infoblox-neutron:
interface: infoblox
scope: container
certificates:
interface: tls-certificates
peers:

View File

@ -75,6 +75,10 @@ global_physnet_mtu = {{ global_physnet_mtu }}
external_dns_driver = designate
{% endif -%}
{% if enable_infoblox -%}
ipam_driver = infoblox
{% endif -%}
{% include "section-zeromq" %}
[quotas]
@ -121,3 +125,7 @@ lock_path = $state_path/lock
{% endif -%}
{% include "section-oslo-middleware" %}
{% if enable_infoblox -%}
{% include "parts/section-infoblox" %}
{% endif -%}

View File

@ -75,6 +75,10 @@ global_physnet_mtu = {{ global_physnet_mtu }}
external_dns_driver = designate
{% endif -%}
{% if enable_infoblox -%}
ipam_driver = infoblox
{% endif -%}
{% include "section-zeromq" %}
[quotas]
@ -120,6 +124,10 @@ lock_path = $state_path/lock
{% include "parts/section-designate" %}
{% endif -%}
{% if enable_infoblox -%}
{% include "parts/section-infoblox" %}
{% endif -%}
[service_providers]
service_provider = LOADBALANCERV2:Haproxy:neutron_lbaas.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default
service_provider = VPN:strongswan:neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default

View File

@ -78,6 +78,10 @@ global_physnet_mtu = {{ global_physnet_mtu }}
external_dns_driver = designate
{% endif -%}
{% if enable_infoblox -%}
ipam_driver = infoblox
{% endif -%}
{% include "parts/section-placement" %}
{% include "section-zeromq" %}
@ -125,6 +129,10 @@ lock_path = $state_path/lock
{% include "parts/section-designate" %}
{% endif -%}
{% if enable_infoblox -%}
{% include "parts/section-infoblox" %}
{% endif -%}
[service_providers]
service_provider = LOADBALANCERV2:Haproxy:neutron_lbaas.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default
service_provider = VPN:strongswan:neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default

View File

@ -0,0 +1,11 @@
[infoblox]
cloud_data_center_id = {{ cloud_data_center_id }}
[infoblox-dc:{{ cloud_data_center_id }}]
grid_master_host = {{ grid_master_host }}
grid_master_name = {{ grid_master_name }}
admin_user_name = {{ infoblox_admin_user_name }}
admin_password = {{ infoblox_admin_password }}
wapi_version = {{ wapi_version }}
wapi_max_results = {{ wapi_max_results }}
wapi_paging = {{ wapi_paging }}

View File

@ -1,5 +1,31 @@
[nova]
{% if enable_infoblox -%}
# TODO - Exceptionally we added the content of [keystone_authtoken] due to an
# internal mechanism of Infoblox plugin lp-1688039.
{% if auth_host -%}
auth_type = password
{% if api_version == "3" -%}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/v3
auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v3
project_domain_name = {{ admin_domain_name }}
user_domain_name = {{ admin_domain_name }}
{% else -%}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}
project_domain_name = default
user_domain_name = default
{% endif -%}
project_name = {{ admin_tenant_name }}
username = {{ admin_user }}
password = {{ admin_password }}
signing_dir = {{ signing_dir }}
{% if use_memcache == true %}
memcached_servers = {{ memcache_url }}
{% endif -%}
{% endif -%}
{% else %}
auth_section = keystone_authtoken
{% endif %}
region_name = {{ region }}
{% if use_internal_endpoints -%}
endpoint_type = internal

View File

@ -78,6 +78,10 @@ global_physnet_mtu = {{ global_physnet_mtu }}
external_dns_driver = designate
{% endif -%}
{% if enable_infoblox -%}
ipam_driver = infoblox
{% endif -%}
{% include "section-zeromq" %}
[quotas]
@ -123,6 +127,10 @@ lock_path = $state_path/lock
{% include "parts/section-designate" %}
{% endif -%}
{% if enable_infoblox -%}
{% include "parts/section-infoblox" %}
{% endif -%}
{% include "parts/section-placement" %}
[service_providers]

View File

@ -78,6 +78,10 @@ global_physnet_mtu = {{ global_physnet_mtu }}
external_dns_driver = designate
{% endif -%}
{% if enable_infoblox -%}
ipam_driver = infoblox
{% endif -%}
{% include "section-zeromq" %}
[quotas]
@ -123,6 +127,10 @@ lock_path = $state_path/lock
{% include "parts/section-designate" %}
{% endif -%}
{% if enable_infoblox -%}
{% include "parts/section-infoblox" %}
{% endif -%}
{% include "parts/section-placement" %}
[service_providers]

View File

@ -1398,3 +1398,54 @@ class NeutronLoadBalancerContextTest(CharmTestCase):
'base_url': 'http://1.2.3.4:1234'})
with self.assertRaises(ValueError):
context.NeutronLoadBalancerContext()()
class NeutronInfobloxContextTest(CharmTestCase):
def setUp(self):
super(NeutronInfobloxContextTest, self).setUp(context, TO_PATCH)
self.relation_get.side_effect = self.test_relation.get
self.config.side_effect = self.test_config.get
def tearDown(self):
super(NeutronInfobloxContextTest, self).tearDown()
def test_infoblox_no_related_units(self):
self.related_units.return_value = []
ctxt = context.NeutronInfobloxContext()()
expect = {}
self.assertEqual(expect, ctxt)
def test_infoblox_related_units(self):
self.related_units.return_value = ['unit1']
self.relation_ids.return_value = ['rid1']
self.test_relation.set(
{'dc_id': '0',
'grid_master_host': 'foo',
'grid_master_name': 'bar',
'admin_user_name': 'faz',
'admin_password': 'baz'})
ctxt = context.NeutronInfobloxContext()()
expect = {'enable_infoblox': True,
'cloud_data_center_id': '0',
'grid_master_host': 'foo',
'grid_master_name': 'bar',
'infoblox_admin_user_name': 'faz',
'infoblox_admin_password': 'baz',
'wapi_version': '2.3',
'wapi_max_results': '-50000',
'wapi_paging': True}
self.assertEqual(expect, ctxt)
def test_infoblox_related_units_missing_data(self):
self.related_units.return_value = ['unit1']
self.relation_ids.return_value = ['rid1']
self.test_relation.set(
{'dc_id': '0',
'grid_master_host': 'foo'})
ctxt = context.NeutronInfobloxContext()()
expect = {}
self.assertEqual(expect, ctxt)

View File

@ -64,6 +64,7 @@ TO_PATCH = [
'get_l2population',
'get_overlay_network_type',
'is_clustered',
'is_leader',
'is_elected_leader',
'is_qos_requested_and_valid',
'is_vlan_trunking_requested_and_valid',
@ -90,7 +91,7 @@ TO_PATCH = [
'remove_old_packages',
'services',
'service_restart',
'generate_ha_relation_data',
'is_db_initialised',
]
NEUTRON_CONF_DIR = "/etc/neutron"
@ -814,7 +815,7 @@ class NeutronAPIHooksTests(CharmTestCase):
self.is_elected_leader.return_value = True
self.os_release.return_value = 'kilo'
hooks.conditional_neutron_migration()
self.migrate_neutron_database.assert_called_with()
self.migrate_neutron_database.assert_called()
def test_conditional_neutron_migration_leader_icehouse(self):
self.test_relation.set({
@ -849,3 +850,17 @@ class NeutronAPIHooksTests(CharmTestCase):
def test_designate_peer_departed(self):
self._call_hook('external-dns-relation-departed')
self.assertTrue(self.CONFIGS.write.called_with(NEUTRON_CONF))
def test_infoblox_peer_changed(self):
self.is_db_initialised.return_value = True
self.test_relation.set({
'dc_id': '0',
})
self.os_release.return_value = 'queens'
self.relation_ids.side_effect = self._fake_relids
self._call_hook('infoblox-neutron-relation-changed')
self.assertTrue(self.CONFIGS.write.called_with(NEUTRON_CONF))
def test_infoblox_peer_departed(self):
self._call_hook('infoblox-neutron-relation-departed')
self.assertTrue(self.CONFIGS.write.called_with(NEUTRON_CONF))