File 0001-keystone-v3-support-in-syn_neutron_to_infoblox.patch of Package openstack-neutron-infoblox

From 2137d2627e1f523101e8245c2c60afa389ccf1fa Mon Sep 17 00:00:00 2001
From: Aliaksandr Dziarkach <adziarkach@infoblox.com>
Date: Wed, 5 Oct 2016 17:26:24 +0300
Subject: [PATCH] keystone v3 support in syn_neutron_to_infoblox

sync_neutron_to_infoblox is assuming keystone v2, and not allowing for
v3-only deployments.
Some customers wants to use keystone v3 configuration.
So sync_neutron_to_infoblox.py fixed to support both keystone v2 and v3.
Verified manaully.

Change-Id: I7f8adecff2c8d613803e13c28db27015080c9548
Closes-Bug: 1606257
Closes-Bug: 1605621
---
 .../neutron/common/keystone_manager.py             |  31 ++-
 .../tests/unit/common/test_keystone_manager.py     | 280 +++++++++++++++++++++
 .../tools/sync_neutron_to_infoblox.py              |  74 +++---
 3 files changed, 351 insertions(+), 34 deletions(-)
 create mode 100644 networking_infoblox/tests/unit/common/test_keystone_manager.py

diff --git a/networking_infoblox/neutron/common/keystone_manager.py b/networking_infoblox/neutron/common/keystone_manager.py
index c3d6d58..f547ebf 100644
--- a/networking_infoblox/neutron/common/keystone_manager.py
+++ b/networking_infoblox/neutron/common/keystone_manager.py
@@ -1,4 +1,4 @@
-# Copyright 2015 Infoblox Inc.
+# Copyright 2016 Infoblox Inc.
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,9 +13,11 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from keystoneclient.auth.identity.generic import token
 from keystoneclient.auth import token_endpoint
 from keystoneclient import session
-from keystoneclient.v2_0 import client as k_client
+from keystoneclient.v2_0 import client as client_2_0
+from keystoneclient.v3 import client as client_3
 from neutron.common import config as cfg
 from oslo_log import log
 
@@ -32,19 +34,36 @@ def init_keystone_session():
     global _SESSION
     if not _SESSION:
         _SESSION = session.Session()
+    return _SESSION
 
 
 def get_keystone_client(auth_token):
-    init_keystone_session()
-    url = CONF['keystone_authtoken']['auth_uri'] + '/v2.0/'
+    sess = init_keystone_session()
+    url = CONF['keystone_authtoken']['auth_uri']
+    # Create token to get available service version
+    generic_token = token.Token(url, token=auth_token)
+    generic_token.reauthenticate = False
+    version = generic_token.get_auth_ref(sess)['version']
+    # update auth url aith version if needed
+    if version not in url.split('/'):
+        url = url + '/' + version
+    # create endpoint token using right url and provided auth token
     auth = token_endpoint.Token(url, auth_token)
-    return k_client.Client(session=session, auth=auth)
+    # create keystone client
+    if version == 'v3':
+        k_client = client_3.Client(session=sess, auth=auth)
+    else:
+        k_client = client_2_0.Client(session=sess, auth=auth)
+    return k_client
 
 
 def get_all_tenants(auth_token):
     try:
         keystone = get_keystone_client(auth_token)
-        return keystone.tenants.list()
+        if keystone.version == 'v3':
+            return keystone.projects.list()
+        else:
+            return keystone.tenants.list()
     except Exception as e:
         LOG.warning("Could not get tenants due to error: %s", e)
     return []
diff --git a/networking_infoblox/tests/unit/common/test_keystone_manager.py b/networking_infoblox/tests/unit/common/test_keystone_manager.py
new file mode 100644
index 0000000..ed4f09d
--- /dev/null
+++ b/networking_infoblox/tests/unit/common/test_keystone_manager.py
@@ -0,0 +1,280 @@
+# Copyright (c) 2016 Infoblox Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mock
+
+from networking_infoblox.neutron.common import keystone_manager
+
+from networking_infoblox.tests import base
+
+
+class TestKeystoneManager(base.TestCase):
+
+    def setUp(self):
+        super(TestKeystoneManager, self).setUp()
+        self.tenants = []
+        for i in range(1, 4):
+            tenant = mock.Mock()
+            tenant.id = str(i)
+            tenant.tenant_id = tenant.id
+            tenant.name = 'tenant_%s' % i
+            self.tenants.append(tenant)
+        self.networks = [
+            {'tenant_id': '1'},
+            {'tenant_id': '2'},
+            {'tenant_id': '3'},
+            {'tenant_id': '4'},
+            ]
+
+    @mock.patch('keystoneclient.session.Session')
+    def test_init_keystone_session(self, SessionMock):
+        keystone_manager._SESSION = None  # reset glogal variable
+        # call function and check that Session created without params
+        session1 = keystone_manager.init_keystone_session()
+        SessionMock.assert_called_once_with()
+        # reset mock, call function again and check that Session is not called
+        # and function return same session
+        SessionMock.reset_mock()
+        session2 = keystone_manager.init_keystone_session()
+        assert not SessionMock.called
+        assert session1 is session2
+
+    @mock.patch('keystoneclient.auth.token_endpoint.Token')
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.CONF')
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                'init_keystone_session')
+    @mock.patch('keystoneclient.auth.identity.generic.token.Token')
+    def _test_get_keystone_client(self, ClientMock, keystone_authtoken,
+                                  TokenMock, InitMock, ConfMock,
+                                  TokenEndpointMock):
+        # prepare mocks
+        session = 'test_session'
+        auth_token = 'test_auth_token'
+        auth_uri = 'test_auth_uri'
+        token_endpoint = 'token_endpoint'
+        InitMock.return_value = session
+        ConfMock.__getitem__ = mock.Mock()
+        ConfMock.__getitem__.return_value = {'auth_uri': auth_uri}
+        TokenObjMock = mock.Mock()
+        TokenObjMock.get_auth_ref.return_value = keystone_authtoken
+        TokenMock.return_value = TokenObjMock
+        TokenEndpointMock.return_value = token_endpoint
+        # call tested function
+        k_client = keystone_manager.get_keystone_client(auth_token)
+        # check calls
+        keystone_manager.init_keystone_session.assert_called_once_with()
+        TokenMock.assert_called_once_with(auth_uri, token=auth_token)
+        TokenObjMock.get_auth_ref.assert_called_once_with(session)
+        TokenEndpointMock.assert_called_once_with(
+            auth_uri + '/' + keystone_authtoken['version'], auth_token)
+        ClientMock.assert_called_once_with(session=session,
+                                           auth=token_endpoint)
+        assert k_client == ClientMock.return_value
+
+    @mock.patch('keystoneclient.v2_0.client.Client')
+    def test_get_keystone_client_v2_0(self, ClientMock):
+        ClientMock.return_value = 'keystone_client_v2_0'
+        self._test_get_keystone_client(ClientMock, {'version': 'v2_0'})
+
+    @mock.patch('keystoneclient.v3.client.Client')
+    def test_get_keystone_client_v3(self, ClientMock):
+        ClientMock.return_value = 'keystone_client_v3'
+        self._test_get_keystone_client(ClientMock, {'version': 'v3'})
+
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                'get_keystone_client')
+    def _test_get_all_tenants(self, version, get_keystone_client_mock):
+        # prepare mocks
+        auth_token = 'test_auth_token'
+        client_mock = mock.MagicMock()
+        client_mock.version = version
+        get_keystone_client_mock.return_value = client_mock
+        # call tested function
+        keystone_manager.get_all_tenants(auth_token)
+        # check calls
+        get_keystone_client_mock.assert_called_once_with(auth_token)
+        if version == 'v3':
+            client_mock.projects.list.assert_called_once_with()
+        else:
+            client_mock.tenants.list.assert_called_once_with()
+
+    def test_get_all_tenants_v2_0(self):
+        self._test_get_all_tenants('v2_0')
+
+    def test_get_all_tenants_v3(self):
+        self._test_get_all_tenants('v3')
+
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                'sync_tenants_from_keystone')
+    @mock.patch('networking_infoblox.neutron.db.infoblox_db.'
+                'add_or_update_tenant')
+    @mock.patch('networking_infoblox.neutron.db.infoblox_db.get_tenants')
+    def _test_update_tenant_mapping(self, auth_token, networks, tenants,
+                                    expected_results,
+                                    get_tenants, add_or_update_tenant,
+                                    sync_tenants_from_keystone):
+        context = mock.Mock()
+        context.session = 'test_session'
+        tenant_id = '1'
+        tenant_name = 'test_tenant_name_1'
+        get_unknown = mock.Mock()
+        get_unknown_params = []
+        func = keystone_manager._get_unknown_ids_from_dict
+
+        def store_params(param):
+            get_unknown_params.append(param.copy())
+            return func(param)
+
+        get_unknown.side_effect = store_params
+        get_tenants.return_value = tenants
+        with mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                        '_get_unknown_ids_from_dict', get_unknown):
+            keystone_manager.update_tenant_mapping(
+                context, networks, tenant_id, tenant_name, auth_token)
+        add_or_update_tenant.assert_called_once_with(context.session,
+                                                     tenant_id, tenant_name)
+        assert get_unknown.call_count == expected_results['get_unknown_count']
+        assert get_unknown_params == expected_results['get_unknown_params']
+        if expected_results['get_tenants_called']:
+            get_tenants.assert_called_once()
+            assert get_tenants.call_args[0] == (context.session,)
+            tenant_ids = expected_results['get_tenant_tenant_ids']
+            assert(
+                sorted(
+                    get_tenants.call_args[1]['tenant_ids']) == tenant_ids)
+        else:
+            get_tenants.assert_not_called()
+        if expected_results['sync_tenants_from_keystone_called']:
+            sync_tenants_from_keystone.assert_called_once_with(context,
+                                                               auth_token)
+        else:
+            sync_tenants_from_keystone.assert_not_called()
+
+    def test_update_tenant_mapping(self):
+        auth_token = 'test_auth_token'
+        networks = self.networks
+        tenants = self.tenants
+        expected_results = {
+            'get_unknown_count': 2,
+            'get_unknown_params': [
+                {
+                    networks[0]['tenant_id']: False,
+                    networks[1]['tenant_id']: True,
+                    networks[2]['tenant_id']: True,
+                    networks[3]['tenant_id']: True
+                },
+                {
+                    networks[0]['tenant_id']: False,
+                    networks[1]['tenant_id']: False,
+                    networks[2]['tenant_id']: False,
+                    networks[3]['tenant_id']: True
+                }],
+            'get_tenants_called': True,
+            'get_tenant_tenant_ids': ['2', '3', '4'],
+            'sync_tenants_from_keystone_called': True
+            }
+        self._test_update_tenant_mapping(auth_token, networks, tenants,
+                                         expected_results)
+
+    def test_update_tenant_mapping_all_tenant_in_db(self):
+        auth_token = 'test_auth_token'
+        networks = self.networks
+        networks[3]['tenant_id'] = networks[0]['tenant_id']
+        tenants = self.tenants
+        expected_results = {
+            'get_unknown_count': 2,
+            'get_unknown_params': [
+                {
+                    networks[0]['tenant_id']: False,
+                    networks[1]['tenant_id']: True,
+                    networks[2]['tenant_id']: True,
+                    networks[3]['tenant_id']: False
+                },
+                {
+                    networks[0]['tenant_id']: False,
+                    networks[1]['tenant_id']: False,
+                    networks[2]['tenant_id']: False,
+                    networks[3]['tenant_id']: False
+                }],
+            'get_tenants_called': True,
+            'get_tenant_tenant_ids': ['2', '3'],
+            'sync_tenants_from_keystone_called': False
+            }
+        self._test_update_tenant_mapping(auth_token, networks, tenants,
+                                         expected_results)
+
+    def test_update_tenant_mapping_all_tenant_in_networks(self):
+        auth_token = 'test_auth_token'
+        networks = self.networks
+        for i in range(1, len(networks)):
+            networks[i]['tenant_id'] = networks[0]['tenant_id']
+        tenants = self.tenants
+        expected_results = {
+            'get_unknown_count': 1,
+            'get_unknown_params': [
+                {
+                    networks[0]['tenant_id']: False,
+                    networks[1]['tenant_id']: False,
+                    networks[2]['tenant_id']: False,
+                    networks[3]['tenant_id']: False
+                }],
+            'get_tenants_called': False,
+            'sync_tenants_from_keystone_called': False
+            }
+        self._test_update_tenant_mapping(auth_token, networks, tenants,
+                                         expected_results)
+
+    def test_update_tenant_mapping_without_token(self):
+        auth_token = ''
+        networks = self.networks
+        tenants = self.tenants
+        expected_results = {
+            'get_unknown_count': 0,
+            'get_unknown_params': [],
+            'get_tenants_called': False,
+            'sync_tenants_from_keystone_called': False
+            }
+        self._test_update_tenant_mapping(auth_token, networks, tenants,
+                                         expected_results)
+
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                'get_all_tenants')
+    @mock.patch('networking_infoblox.neutron.db.infoblox_db.'
+                'add_or_update_tenant')
+    def test_sync_tenants_from_keystone(self, AddTenantMock, get_all_tenants):
+        # prepare test data
+        context = mock.Mock()
+        context.session = 'test_session'
+        auth_token = 'test_auth_token'
+        get_all_tenants.return_value = self.tenants
+        # call tested function
+        ret = keystone_manager.sync_tenants_from_keystone(context, auth_token)
+        # check return value and calls
+        assert ret == len(self.tenants)
+        get_all_tenants.assert_called_once_with(auth_token)
+        expected_call_list = [
+            mock.call(context.session, t.id, t.name) for t in self.tenants]
+        assert AddTenantMock.call_args_list == expected_call_list
+
+    @mock.patch('networking_infoblox.neutron.common.keystone_manager.'
+                'get_all_tenants')
+    @mock.patch('networking_infoblox.neutron.db.infoblox_db.'
+                'add_or_update_tenant')
+    def test_sync_tenants_from_keystone_without_token(self, AddTenantMock,
+                                                      get_all_tenants):
+        ret = keystone_manager.sync_tenants_from_keystone('context', None)
+        assert ret is None
+        get_all_tenants.assert_not_called()
+        AddTenantMock.assert_not_called
diff --git a/networking_infoblox/tools/sync_neutron_to_infoblox.py b/networking_infoblox/tools/sync_neutron_to_infoblox.py
index f0b30ad..2551c47 100644
--- a/networking_infoblox/tools/sync_neutron_to_infoblox.py
+++ b/networking_infoblox/tools/sync_neutron_to_infoblox.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2015 Infoblox Inc.
+# Copyright 2016 Infoblox Inc.
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -17,15 +17,17 @@
 import os
 import sys
 
-from keystoneclient.v2_0 import client as ksclient
+from keystoneauth1.identity import v2
+from keystoneauth1.identity import v3
+from keystoneauth1 import session as ks_session
+from keystoneclient.v2_0 import client as client_2_0
+from keystoneclient.v3 import client as client_3
 
 from oslo_config import cfg
 from oslo_log import log as logging
 
 from neutron.common import config as common_config
 from neutron import context as neutron_context
-from neutron.i18n import _LE
-from neutron.i18n import _LW
 
 from neutronclient.v2_0 import client as neutron_client
 from novaclient import client as nova_client
@@ -33,6 +35,8 @@ from novaclient import client as nova_client
 from infoblox_client import exceptions as ib_exc
 from infoblox_client import objects as ib_objects
 
+from networking_infoblox._i18n import _LE
+from networking_infoblox._i18n import _LW
 from networking_infoblox.neutron.common import config
 from networking_infoblox.neutron.common import context as ib_context
 from networking_infoblox.neutron.common import dns
@@ -71,20 +75,38 @@ def main():
     config.register_infoblox_grid_opts(cfg.CONF, grid_id)
     register_keystone_opts(cfg.CONF)
 
-    credentials = get_credentials()
-    if not credentials:
+    try:
+        credentials, version = get_credentials()
+    except KeyError:
         print("\nYou must provide an admin user credentials in the shell "
               "environment.\nPlease export variables such as env[OS_USERNAME],"
               " env[OS_PASSWORD], env[OS_AUTH_URL], env[OS_TENANT_NAME], and "
               "env[OS_REGION_NAME]\n")
         return
 
+    password_creds = credentials.copy()
+    password_creds.pop('region_name', None)
+    if version == '3':
+        auth = v3.Password(**password_creds)
+        session = ks_session.Session(auth=auth)
+        client = client_3.Client(session=session)
+    else:
+        auth = v2.Password(**password_creds)
+        session = ks_session.Session(auth=auth)
+        client = client_2_0.Client(session=session)
+
     context = neutron_context.get_admin_context()
-    context.auth_token = get_auth_token(credentials)
+    context.auth_token = client.ec2.client.get_token()
+    context.user_id = client.ec2.client.get_user_id()
+    context.tenant_id = client.ec2.client.get_project_id()
 
     grid_manager = grid.GridManager(context)
     grid_manager.sync(force_sync=True)
 
+    credentials['session'] = session
+    for key in ('user_domain_id', 'project_domain_id'):
+        credentials.pop(key, None)
+
     sync_neutron_to_infoblox(context, credentials, grid_manager)
 
 
@@ -103,23 +125,22 @@ def register_keystone_opts(conf):
 
 def get_credentials():
     d = dict()
-    if ('OS_USERNAME' in os.environ and
-            'OS_PASSWORD' in os.environ and
-            'OS_AUTH_URL' in os.environ and
-            'OS_TENANT_NAME' in os.environ and
-            'OS_REGION_NAME' in os.environ):
-        d['username'] = os.environ['OS_USERNAME']
-        d['password'] = os.environ['OS_PASSWORD']
-        d['auth_url'] = os.environ['OS_AUTH_URL']
+    version = '2'
+    if 'OS_IDENTITY_API_VERSION' in os.environ:
+        version = os.environ['OS_IDENTITY_API_VERSION']
+    if version == '3':
+        d['project_name'] = os.environ['OS_PROJECT_NAME']
+        d['user_domain_id'] = os.environ.get('OS_USER_DOMAIN_ID', 'default')
+        d['project_domain_id'] = os.environ.get('OS_PROJECT_DOMAIN_ID',
+                                                'default')
+    else:
         d['tenant_name'] = os.environ['OS_TENANT_NAME']
-        d['region_name'] = os.environ['OS_REGION_NAME']
-    return d
-
-
-def get_auth_token(credentials):
-    keystone = ksclient.Client(**credentials)
+    d['username'] = os.environ['OS_USERNAME']
+    d['password'] = os.environ['OS_PASSWORD']
+    d['auth_url'] = os.environ['OS_AUTH_URL']
+    d['region_name'] = os.environ['OS_REGION_NAME']
 
-    return keystone.auth_ref['token']['id']
+    return d, version
 
 
 def sync_neutron_to_infoblox(context, credentials, grid_manager):
@@ -154,10 +175,7 @@ def sync_neutron_to_infoblox(context, credentials, grid_manager):
     payload = neutron_api.list_ports()
     ports = payload['ports']
     nova_api = nova_client.Client(NOVA_API_VERSION,
-                                  credentials['username'],
-                                  credentials['password'],
-                                  credentials['tenant_name'],
-                                  credentials['auth_url'])
+                                  session=credentials['session'])
 
     instance_names_by_instance_id = dict()
     instance_names_by_floating_ip = dict()
@@ -170,8 +188,8 @@ def sync_neutron_to_infoblox(context, credentials, grid_manager):
         for fip in floating_ips:
             instance_names_by_floating_ip[fip] = server.name
 
-    user_id = context.user_id or neutron_api.httpclient.auth_ref['user']['id']
-    user_tenant_id = context.tenant_id or neutron_api.httpclient.auth_tenant_id
+    user_id = neutron_api.httpclient.get_user_id()
+    user_tenant_id = neutron_api.httpclient.get_project_id()
 
     ib_networks = []
     should_exit = False
-- 
2.14.1

openSUSE Build Service is sponsored by