diff --git a/hooks/infoblox-neutron-relation-broken b/hooks/infoblox-neutron-relation-broken new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/infoblox-neutron-relation-broken @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/infoblox-neutron-relation-changed b/hooks/infoblox-neutron-relation-changed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/infoblox-neutron-relation-changed @@ -0,0 +1 @@ +neutron_api_hooks.py \ No newline at end of file diff --git a/hooks/infoblox-neutron-relation-departed b/hooks/infoblox-neutron-relation-departed new file mode 120000 index 00000000..1fb10fd5 --- /dev/null +++ b/hooks/infoblox-neutron-relation-departed @@ -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 13888ec6..46e99441 100644 --- a/hooks/neutron_api_context.py +++ b/hooks/neutron_api_context.py @@ -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) diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index 6ee4d2bf..10c3b582 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -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() diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index 665c1e5e..6b63d3dd 100755 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -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'], diff --git a/metadata.yaml b/metadata.yaml index 187ed960..666f5ac4 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -55,6 +55,9 @@ requires: interface: midonet external-dns: interface: designate + infoblox-neutron: + interface: infoblox + scope: container certificates: interface: tls-certificates peers: diff --git a/templates/mitaka/neutron.conf b/templates/mitaka/neutron.conf index 79fabf20..c59e7407 100644 --- a/templates/mitaka/neutron.conf +++ b/templates/mitaka/neutron.conf @@ -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 -%} diff --git a/templates/newton/neutron.conf b/templates/newton/neutron.conf index c07e31f6..4aa662bb 100644 --- a/templates/newton/neutron.conf +++ b/templates/newton/neutron.conf @@ -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 diff --git a/templates/ocata/neutron.conf b/templates/ocata/neutron.conf index f572756a..cc2f75b5 100644 --- a/templates/ocata/neutron.conf +++ b/templates/ocata/neutron.conf @@ -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 diff --git a/templates/parts/section-infoblox b/templates/parts/section-infoblox new file mode 100644 index 00000000..ac2a4709 --- /dev/null +++ b/templates/parts/section-infoblox @@ -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 }} diff --git a/templates/parts/section-nova b/templates/parts/section-nova index 4a0e4eae..65f3b474 100644 --- a/templates/parts/section-nova +++ b/templates/parts/section-nova @@ -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 diff --git a/templates/pike/neutron.conf b/templates/pike/neutron.conf index 09a795fa..291b1ad0 100644 --- a/templates/pike/neutron.conf +++ b/templates/pike/neutron.conf @@ -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] diff --git a/templates/queens/neutron.conf b/templates/queens/neutron.conf index 4fb19b43..8328cacb 100644 --- a/templates/queens/neutron.conf +++ b/templates/queens/neutron.conf @@ -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] diff --git a/unit_tests/test_neutron_api_context.py b/unit_tests/test_neutron_api_context.py index 4f103a61..a7ec3553 100644 --- a/unit_tests/test_neutron_api_context.py +++ b/unit_tests/test_neutron_api_context.py @@ -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) diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index f59d6e98..ce150fb0 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -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))