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