From fd09a0cfc392349ec3e7437c86aabef40c574cd4 Mon Sep 17 00:00:00 2001 From: elajkat Date: Wed, 22 Feb 2023 14:20:28 +0100 Subject: [PATCH] Use SDK instead of neutronclient The python-neutronclient has been deprecated for the CLI since Ocata and the python bindings "neutronclient" has been deprecated for removal as of the 2023.1 (Antelope) release[1] in favor of using openstacksdk. This patch migrates Designate from using the neutronclient to using the openstacksdk for communicating with neutron. [1] https://docs.openstack.org/releasenotes/python-neutronclient/2023.1.html Co-Authored-By: Michael Johnson Change-Id: I0198f38afe3d5c32ea06d9e674ab0ff849f360e6 Related-Bug: #1999774 --- designate/conf/network_api.py | 45 +++++-- designate/network_api/neutron.py | 51 ++++---- .../tests/unit/network_api/test_neutron.py | 121 ++++++++++-------- ...nt-with-openstacksdk-5ae199bc327376b9.yaml | 10 ++ requirements.txt | 2 +- 5 files changed, 141 insertions(+), 88 deletions(-) create mode 100644 releasenotes/notes/Replace-neutronclient-with-openstacksdk-5ae199bc327376b9.yaml diff --git a/designate/conf/network_api.py b/designate/conf/network_api.py index 3c6aaf5cc..202e2255e 100644 --- a/designate/conf/network_api.py +++ b/designate/conf/network_api.py @@ -24,25 +24,44 @@ NETWORK_API_NEUTRON_OPTS = [ cfg.IntOpt('timeout', default=30, help='timeout value for connecting to neutron in seconds'), - cfg.StrOpt('admin_username', - help='username for connecting to neutron in admin context'), - cfg.StrOpt('admin_password', - help='password for connecting to neutron in admin context', - secret=True), - cfg.StrOpt('admin_tenant_name', - help='tenant name for connecting to neutron in admin context'), - cfg.StrOpt('auth_url', - help='auth url for connecting to neutron in admin context'), cfg.BoolOpt('insecure', default=False, help='if set, ignore any SSL validation issues'), - cfg.StrOpt('auth_strategy', - default='keystone', - help='auth strategy for connecting to ' - 'neutron in admin context'), cfg.StrOpt('ca_certificates_file', help='Location of ca certificates file to use for ' 'neutron client requests.'), + cfg.StrOpt('client_certificate_file', + help='Location of client certificate file to use for ' + 'neutron client requests.'), + + + cfg.StrOpt('admin_username', + help='username for connecting to neutron in admin context', + deprecated_for_removal=True, + deprecated_reason='This parameter is no longer used.', + deprecated_since='2023.2'), + cfg.StrOpt('admin_password', + help='password for connecting to neutron in admin context', + secret=True, deprecated_for_removal=True, + deprecated_reason='This parameter is no longer used.', + deprecated_since='2023.2'), + cfg.StrOpt('admin_tenant_name', + help='tenant name for connecting to neutron in admin context', + deprecated_for_removal=True, + deprecated_reason='This parameter is no longer used.', + deprecated_since='2023.2'), + cfg.StrOpt('auth_url', + help='auth url for connecting to neutron in admin context', + deprecated_for_removal=True, + deprecated_reason='This parameter is no longer used.', + deprecated_since='2023.2'), + cfg.StrOpt('auth_strategy', + default='keystone', + help='auth strategy for connecting to ' + 'neutron in admin context', + deprecated_for_removal=True, + deprecated_reason='This parameter is no longer used.', + deprecated_since='2023.2'), ] diff --git a/designate/network_api/neutron.py b/designate/network_api/neutron.py index 111da5a27..5f22da133 100644 --- a/designate/network_api/neutron.py +++ b/designate/network_api/neutron.py @@ -16,36 +16,39 @@ # Copied partially from nova import concurrent.futures import futurist -from neutronclient.common import exceptions as neutron_exceptions -from neutronclient.v2_0 import client as clientv20 +from keystoneauth1 import session +from keystoneauth1 import token_endpoint +import openstack +from openstack import exceptions as sdk_exceptions from oslo_config import cfg from oslo_log import log as logging from designate import exceptions from designate.network_api import base +from designate import version CONF = cfg.CONF LOG = logging.getLogger(__name__) def get_client(context, endpoint): - params = { - 'endpoint_url': endpoint, - 'timeout': CONF['network_api:neutron'].timeout, - 'insecure': CONF['network_api:neutron'].insecure, - 'ca_cert': CONF['network_api:neutron'].ca_certificates_file, - } + verify = True + if CONF['network_api:neutron'].insecure: + verify = False + elif CONF['network_api:neutron'].ca_certificates_file: + verify = CONF['network_api:neutron'].ca_certificates_file - if context.auth_token: - params['token'] = context.auth_token - params['auth_strategy'] = None - elif CONF['network_api:neutron'].admin_username is not None: - params['username'] = CONF['network_api:neutron'].admin_username - params['project_name'] = CONF['network_api:neutron'].admin_tenant_name - params['password'] = CONF['network_api:neutron'].admin_password - params['auth_url'] = CONF['network_api:neutron'].auth_url - params['auth_strategy'] = CONF['network_api:neutron'].auth_strategy - return clientv20.Client(**params) + auth_token = token_endpoint.Token(endpoint, context.auth_token) + + user_session = session.Session( + auth=auth_token, + verify=verify, + cert=CONF['network_api:neutron'].client_certificate_file, + timeout=CONF['network_api:neutron'].timeout, + app_name='designate', + app_version=version.version_info.version_string()) + + return openstack.connection.Connection(session=user_session) class NeutronNetworkAPI(base.NetworkAPI): @@ -91,18 +94,20 @@ class NeutronNetworkAPI(base.NetworkAPI): {'region': region, 'endpoint': endpoint}) client = get_client(context, endpoint=endpoint) try: - fips = client.list_floatingips(project_id=project_id) - for fip in fips['floatingips']: + fips = client.network.ips(project_id=project_id) + for fip in fips: yield { 'id': fip['id'], 'address': fip['floating_ip_address'], 'region': region } - except neutron_exceptions.Unauthorized: + except sdk_exceptions.HttpException as http_ex: LOG.warning( 'Failed fetching floating ips from %(region)s @ %(endpoint)s' - 'due to an Unauthorized error', - {'region': region, 'endpoint': endpoint} + 'due to a %(cause)s error', + {'region': region, + 'endpoint': endpoint, + 'cause': http_ex.message} ) except Exception: LOG.error( diff --git a/designate/tests/unit/network_api/test_neutron.py b/designate/tests/unit/network_api/test_neutron.py index 243bc2cef..cdc11ddc9 100644 --- a/designate/tests/unit/network_api/test_neutron.py +++ b/designate/tests/unit/network_api/test_neutron.py @@ -15,8 +15,7 @@ # under the License. from unittest import mock -from neutronclient.common import exceptions as neutron_exceptions -from neutronclient.v2_0 import client as clientv20 +from openstack import exceptions as sdk_exceptions from oslo_config import cfg from oslo_config import fixture as cfg_fixture import oslotest.base @@ -25,6 +24,7 @@ from designate import context from designate import exceptions from designate.network_api import get_network_api from designate.network_api import neutron +from designate import version CONF = cfg.CONF @@ -35,75 +35,94 @@ class NeutronNetworkAPITest(oslotest.base.BaseTestCase): self.useFixture(cfg_fixture.Config(CONF)) CONF.set_override( - 'endpoints', ['RegionOne|http://localhost:9696'], + 'endpoints', ['RegionOne|http://192.0.2.5:9696'], 'network_api:neutron' ) + self.ca_certificates_file = 'fake_ca_cert_file' + self.client_certificate_file = 'fake_client_cert_file' + CONF.set_override('client_certificate_file', + self.client_certificate_file, + 'network_api:neutron') + self.neutron_timeout = 100 + CONF.set_override('timeout', self.neutron_timeout, + 'network_api:neutron') self.api = get_network_api('neutron') self.context = context.DesignateContext( user_id='12345', project_id='54321', ) - @mock.patch.object(clientv20, 'Client') - def test_get_client(self, mock_client): - neutron.get_client(self.context, 'http://localhost:9696') + @mock.patch('keystoneauth1.token_endpoint.Token') + @mock.patch('keystoneauth1.session.Session') + @mock.patch('openstack.connection.Connection') + def test_get_client(self, mock_client, mock_session, mock_token): + auth_token_mock = mock.MagicMock() + mock_token.return_value = auth_token_mock - _, kwargs = mock_client.call_args + user_session_mock = mock.MagicMock() + mock_session.return_value = user_session_mock - self.assertIn('endpoint_url', kwargs) - self.assertIn('timeout', kwargs) - self.assertIn('insecure', kwargs) - self.assertIn('ca_cert', kwargs) + connection_mock = mock.MagicMock() + mock_client.return_value = connection_mock - self.assertNotIn('token', kwargs) - self.assertNotIn('username', kwargs) - - self.assertEqual('http://localhost:9696', kwargs['endpoint_url']) - - @mock.patch.object(clientv20, 'Client') - def test_get_client_using_token(self, mock_client): self.context = context.DesignateContext( user_id='12345', project_id='54321', auth_token='token', ) + endpoint = 'http://192.0.2.5:9696' - neutron.get_client(self.context, 'http://localhost:9696') + result = neutron.get_client(self.context, endpoint) - _, kwargs = mock_client.call_args + mock_token.assert_called_once_with(endpoint, self.context.auth_token) - self.assertIn('token', kwargs) - self.assertIn('auth_strategy', kwargs) - self.assertNotIn('username', kwargs) + mock_session.assert_called_once_with( + auth=auth_token_mock, verify=True, + cert=self.client_certificate_file, timeout=self.neutron_timeout, + app_name='designate', + app_version=version.version_info.version_string()) - self.assertEqual('http://localhost:9696', kwargs['endpoint_url']) - self.assertEqual(self.context.auth_token, kwargs['token']) + self.assertEqual(connection_mock, result) - @mock.patch.object(clientv20, 'Client') - def test_get_client_using_admin(self, mock_client): - CONF.set_override( - 'admin_username', 'test', - 'network_api:neutron' - ) + # Test with CA certs file configuration + mock_token.reset_mock() + mock_session.reset_mock() - neutron.get_client(self.context, 'http://localhost:9696') + CONF.set_override('ca_certificates_file', self.ca_certificates_file, + 'network_api:neutron') - _, kwargs = mock_client.call_args + result = neutron.get_client(self.context, endpoint) - self.assertIn('auth_strategy', kwargs) - self.assertIn('username', kwargs) - self.assertIn('project_name', kwargs) - self.assertIn('password', kwargs) - self.assertIn('auth_url', kwargs) - self.assertNotIn('token', kwargs) + mock_token.assert_called_once_with(endpoint, self.context.auth_token) - self.assertEqual('http://localhost:9696', kwargs['endpoint_url']) - self.assertEqual( - kwargs['username'], CONF['network_api:neutron'].admin_username - ) + mock_session.assert_called_once_with( + auth=auth_token_mock, verify=self.ca_certificates_file, + cert=self.client_certificate_file, timeout=self.neutron_timeout, + app_name='designate', + app_version=version.version_info.version_string()) - @mock.patch.object(neutron, 'get_client') + self.assertEqual(connection_mock, result) + + # Test with insecure configuration + mock_token.reset_mock() + mock_session.reset_mock() + + CONF.set_override('insecure', True, 'network_api:neutron') + + result = neutron.get_client(self.context, endpoint) + + mock_token.assert_called_once_with(endpoint, self.context.auth_token) + + mock_session.assert_called_once_with( + auth=auth_token_mock, verify=False, + cert=self.client_certificate_file, timeout=self.neutron_timeout, + app_name='designate', + app_version=version.version_info.version_string()) + + self.assertEqual(connection_mock, result) + + @mock.patch('designate.network_api.neutron.get_client') def test_list_floatingips(self, get_client): driver = mock.Mock() - driver.list_floatingips.return_value = {'floatingips': [ + driver.network.ips.return_value = [ { 'id': '123', 'floating_ip_address': '192.168.0.100', @@ -114,24 +133,24 @@ class NeutronNetworkAPITest(oslotest.base.BaseTestCase): 'floating_ip_address': '192.168.0.200', 'region': 'RegionOne' }, - ]} + ] get_client.return_value = driver self.assertEqual(2, len(self.api.list_floatingips(self.context))) - @mock.patch.object(neutron, 'get_client') + @mock.patch('designate.network_api.neutron.get_client') def test_list_floatingips_unauthorized(self, get_client): driver = mock.Mock() - driver.list_floatingips.side_effect = neutron_exceptions.Unauthorized + driver.network.ips.side_effect = sdk_exceptions.HttpException get_client.return_value = driver self.assertEqual(0, len(self.api.list_floatingips(self.context))) - @mock.patch.object(neutron, 'get_client') + @mock.patch('designate.network_api.neutron.get_client') def test_list_floatingips_communication_failure(self, get_client): driver = mock.Mock() - driver.list_floatingips.side_effect = ( - neutron_exceptions.NeutronException + driver.network.ips.side_effect = ( + Exception ) get_client.return_value = driver diff --git a/releasenotes/notes/Replace-neutronclient-with-openstacksdk-5ae199bc327376b9.yaml b/releasenotes/notes/Replace-neutronclient-with-openstacksdk-5ae199bc327376b9.yaml new file mode 100644 index 000000000..5e6c67e43 --- /dev/null +++ b/releasenotes/notes/Replace-neutronclient-with-openstacksdk-5ae199bc327376b9.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + Designate will now use the openstacksdk to access neutron instead of the + deprecated neutronclient. The python-neutronclient module requirement has + been replaced by the openstacksdk module. +other: + - | + Designate will now use the openstacksdk to access neutron instead of the + deprecated neutronclient. diff --git a/requirements.txt b/requirements.txt index b16f80945..e51529594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ Jinja2>=2.10 # BSD License (3 clause) jsonschema>=3.2.0 # MIT keystoneauth1>=3.4.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 +openstacksdk>=0.103.0 # Apache-2.0 oslo.config>=6.8.0 # Apache-2.0 oslo.concurrency>=4.2.0 # Apache-2.0 oslo.messaging>=14.1.0 # Apache-2.0 @@ -31,7 +32,6 @@ PasteDeploy>=1.5.0 # MIT pbr>=3.1.1 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD python-designateclient>=2.12.0 # Apache-2.0 -python-neutronclient>=6.7.0 # Apache-2.0 requests>=2.23.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0 SQLAlchemy>=1.4.41 # MIT