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 <johnsomor@gmail.com>
Change-Id: I0198f38afe3d5c32ea06d9e674ab0ff849f360e6
Related-Bug: #1999774
This commit is contained in:
elajkat 2023-02-22 14:20:28 +01:00 committed by Erik Olof Gunnar Andersson
parent b8ec3b450b
commit fd09a0cfc3
5 changed files with 141 additions and 88 deletions

View File

@ -24,25 +24,44 @@ NETWORK_API_NEUTRON_OPTS = [
cfg.IntOpt('timeout', cfg.IntOpt('timeout',
default=30, default=30,
help='timeout value for connecting to neutron in seconds'), 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', cfg.BoolOpt('insecure',
default=False, default=False,
help='if set, ignore any SSL validation issues'), 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', cfg.StrOpt('ca_certificates_file',
help='Location of ca certificates file to use for ' help='Location of ca certificates file to use for '
'neutron client requests.'), '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'),
] ]

View File

@ -16,36 +16,39 @@
# Copied partially from nova # Copied partially from nova
import concurrent.futures import concurrent.futures
import futurist import futurist
from neutronclient.common import exceptions as neutron_exceptions from keystoneauth1 import session
from neutronclient.v2_0 import client as clientv20 from keystoneauth1 import token_endpoint
import openstack
from openstack import exceptions as sdk_exceptions
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from designate import exceptions from designate import exceptions
from designate.network_api import base from designate.network_api import base
from designate import version
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def get_client(context, endpoint): def get_client(context, endpoint):
params = { verify = True
'endpoint_url': endpoint, if CONF['network_api:neutron'].insecure:
'timeout': CONF['network_api:neutron'].timeout, verify = False
'insecure': CONF['network_api:neutron'].insecure, elif CONF['network_api:neutron'].ca_certificates_file:
'ca_cert': CONF['network_api:neutron'].ca_certificates_file, verify = CONF['network_api:neutron'].ca_certificates_file
}
if context.auth_token: auth_token = token_endpoint.Token(endpoint, context.auth_token)
params['token'] = context.auth_token
params['auth_strategy'] = None user_session = session.Session(
elif CONF['network_api:neutron'].admin_username is not None: auth=auth_token,
params['username'] = CONF['network_api:neutron'].admin_username verify=verify,
params['project_name'] = CONF['network_api:neutron'].admin_tenant_name cert=CONF['network_api:neutron'].client_certificate_file,
params['password'] = CONF['network_api:neutron'].admin_password timeout=CONF['network_api:neutron'].timeout,
params['auth_url'] = CONF['network_api:neutron'].auth_url app_name='designate',
params['auth_strategy'] = CONF['network_api:neutron'].auth_strategy app_version=version.version_info.version_string())
return clientv20.Client(**params)
return openstack.connection.Connection(session=user_session)
class NeutronNetworkAPI(base.NetworkAPI): class NeutronNetworkAPI(base.NetworkAPI):
@ -91,18 +94,20 @@ class NeutronNetworkAPI(base.NetworkAPI):
{'region': region, 'endpoint': endpoint}) {'region': region, 'endpoint': endpoint})
client = get_client(context, endpoint=endpoint) client = get_client(context, endpoint=endpoint)
try: try:
fips = client.list_floatingips(project_id=project_id) fips = client.network.ips(project_id=project_id)
for fip in fips['floatingips']: for fip in fips:
yield { yield {
'id': fip['id'], 'id': fip['id'],
'address': fip['floating_ip_address'], 'address': fip['floating_ip_address'],
'region': region 'region': region
} }
except neutron_exceptions.Unauthorized: except sdk_exceptions.HttpException as http_ex:
LOG.warning( LOG.warning(
'Failed fetching floating ips from %(region)s @ %(endpoint)s' 'Failed fetching floating ips from %(region)s @ %(endpoint)s'
'due to an Unauthorized error', 'due to a %(cause)s error',
{'region': region, 'endpoint': endpoint} {'region': region,
'endpoint': endpoint,
'cause': http_ex.message}
) )
except Exception: except Exception:
LOG.error( LOG.error(

View File

@ -15,8 +15,7 @@
# under the License. # under the License.
from unittest import mock from unittest import mock
from neutronclient.common import exceptions as neutron_exceptions from openstack import exceptions as sdk_exceptions
from neutronclient.v2_0 import client as clientv20
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture as cfg_fixture from oslo_config import fixture as cfg_fixture
import oslotest.base import oslotest.base
@ -25,6 +24,7 @@ from designate import context
from designate import exceptions from designate import exceptions
from designate.network_api import get_network_api from designate.network_api import get_network_api
from designate.network_api import neutron from designate.network_api import neutron
from designate import version
CONF = cfg.CONF CONF = cfg.CONF
@ -35,75 +35,94 @@ class NeutronNetworkAPITest(oslotest.base.BaseTestCase):
self.useFixture(cfg_fixture.Config(CONF)) self.useFixture(cfg_fixture.Config(CONF))
CONF.set_override( CONF.set_override(
'endpoints', ['RegionOne|http://localhost:9696'], 'endpoints', ['RegionOne|http://192.0.2.5:9696'],
'network_api:neutron' '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.api = get_network_api('neutron')
self.context = context.DesignateContext( self.context = context.DesignateContext(
user_id='12345', project_id='54321', user_id='12345', project_id='54321',
) )
@mock.patch.object(clientv20, 'Client') @mock.patch('keystoneauth1.token_endpoint.Token')
def test_get_client(self, mock_client): @mock.patch('keystoneauth1.session.Session')
neutron.get_client(self.context, 'http://localhost:9696') @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) connection_mock = mock.MagicMock()
self.assertIn('timeout', kwargs) mock_client.return_value = connection_mock
self.assertIn('insecure', kwargs)
self.assertIn('ca_cert', kwargs)
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( self.context = context.DesignateContext(
user_id='12345', project_id='54321', auth_token='token', 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) mock_session.assert_called_once_with(
self.assertIn('auth_strategy', kwargs) auth=auth_token_mock, verify=True,
self.assertNotIn('username', kwargs) 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(connection_mock, result)
self.assertEqual(self.context.auth_token, kwargs['token'])
@mock.patch.object(clientv20, 'Client') # Test with CA certs file configuration
def test_get_client_using_admin(self, mock_client): mock_token.reset_mock()
CONF.set_override( mock_session.reset_mock()
'admin_username', 'test',
'network_api:neutron'
)
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) mock_token.assert_called_once_with(endpoint, self.context.auth_token)
self.assertIn('username', kwargs)
self.assertIn('project_name', kwargs)
self.assertIn('password', kwargs)
self.assertIn('auth_url', kwargs)
self.assertNotIn('token', kwargs)
self.assertEqual('http://localhost:9696', kwargs['endpoint_url']) mock_session.assert_called_once_with(
self.assertEqual( auth=auth_token_mock, verify=self.ca_certificates_file,
kwargs['username'], CONF['network_api:neutron'].admin_username 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): def test_list_floatingips(self, get_client):
driver = mock.Mock() driver = mock.Mock()
driver.list_floatingips.return_value = {'floatingips': [ driver.network.ips.return_value = [
{ {
'id': '123', 'id': '123',
'floating_ip_address': '192.168.0.100', 'floating_ip_address': '192.168.0.100',
@ -114,24 +133,24 @@ class NeutronNetworkAPITest(oslotest.base.BaseTestCase):
'floating_ip_address': '192.168.0.200', 'floating_ip_address': '192.168.0.200',
'region': 'RegionOne' 'region': 'RegionOne'
}, },
]} ]
get_client.return_value = driver get_client.return_value = driver
self.assertEqual(2, len(self.api.list_floatingips(self.context))) 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): def test_list_floatingips_unauthorized(self, get_client):
driver = mock.Mock() driver = mock.Mock()
driver.list_floatingips.side_effect = neutron_exceptions.Unauthorized driver.network.ips.side_effect = sdk_exceptions.HttpException
get_client.return_value = driver get_client.return_value = driver
self.assertEqual(0, len(self.api.list_floatingips(self.context))) 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): def test_list_floatingips_communication_failure(self, get_client):
driver = mock.Mock() driver = mock.Mock()
driver.list_floatingips.side_effect = ( driver.network.ips.side_effect = (
neutron_exceptions.NeutronException Exception
) )
get_client.return_value = driver get_client.return_value = driver

View File

@ -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.

View File

@ -13,6 +13,7 @@ Jinja2>=2.10 # BSD License (3 clause)
jsonschema>=3.2.0 # MIT jsonschema>=3.2.0 # MIT
keystoneauth1>=3.4.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0
keystonemiddleware>=4.17.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.config>=6.8.0 # Apache-2.0
oslo.concurrency>=4.2.0 # Apache-2.0 oslo.concurrency>=4.2.0 # Apache-2.0
oslo.messaging>=14.1.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 pbr>=3.1.1 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD 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-designateclient>=2.12.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
requests>=2.23.0 # Apache-2.0 requests>=2.23.0 # Apache-2.0
tenacity>=6.0.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0
SQLAlchemy>=1.4.41 # MIT SQLAlchemy>=1.4.41 # MIT