From 92a10628303e9344385b9ad0c92661a9bb3c0b2e Mon Sep 17 00:00:00 2001 From: Michael Skalka <michael.skalka@canonical.com> Date: Wed, 6 Jun 2018 12:30:56 -0400 Subject: [PATCH] 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 --- hooks/infoblox-neutron-relation-broken | 1 + hooks/infoblox-neutron-relation-changed | 1 + hooks/infoblox-neutron-relation-departed | 1 + hooks/neutron_api_context.py | 40 +++++++++++++++++++ hooks/neutron_api_hooks.py | 34 +++++++++++++++- hooks/neutron_api_utils.py | 3 +- metadata.yaml | 3 ++ templates/mitaka/neutron.conf | 8 ++++ templates/newton/neutron.conf | 8 ++++ templates/ocata/neutron.conf | 8 ++++ templates/parts/section-infoblox | 11 +++++ templates/parts/section-nova | 26 ++++++++++++ templates/pike/neutron.conf | 8 ++++ templates/queens/neutron.conf | 8 ++++ unit_tests/test_neutron_api_context.py | 51 ++++++++++++++++++++++++ unit_tests/test_neutron_api_hooks.py | 19 ++++++++- 16 files changed, 226 insertions(+), 4 deletions(-) create mode 120000 hooks/infoblox-neutron-relation-broken create mode 120000 hooks/infoblox-neutron-relation-changed create mode 120000 hooks/infoblox-neutron-relation-departed create mode 100644 templates/parts/section-infoblox 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))