From 7f145e703cfca618465211e95efbe73ac143858d Mon Sep 17 00:00:00 2001 From: Paulo Ewerton Date: Wed, 27 Jan 2016 18:03:07 +0000 Subject: [PATCH] Refactor Keystone client with keystoneauth This patch does, basically, three things: * Updates the default auth section to keystone_auth; * Introduces keystoneauth sessions and plugins; * Adds a deprecation warning and options when loading legacy auth. Config, tests and client code are also updated. Co-Authored-By: Henrique Truta Co-Authored-By: Raildo Mascena Closes-Bug: 1496810 Closes-Bug: 1515014 Change-Id: I5c1cd24ca28d66ae7ae40e7f707b81870cf0e457 --- devstack/lib/magnum | 10 ++ magnum/common/clients.py | 14 +- magnum/common/keystone.py | 166 ++++++++++++---------- magnum/opts.py | 1 + magnum/tests/unit/common/test_clients.py | 26 ++-- magnum/tests/unit/common/test_keystone.py | 138 ++++++++++-------- requirements.txt | 1 + 7 files changed, 201 insertions(+), 155 deletions(-) diff --git a/devstack/lib/magnum b/devstack/lib/magnum index a7870da181..46ab92b562 100644 --- a/devstack/lib/magnum +++ b/devstack/lib/magnum @@ -139,12 +139,22 @@ function create_magnum_conf { iniset $MAGNUM_CONF oslo_policy policy_file $MAGNUM_POLICY_JSON + iniset $MAGNUM_CONF keystone_auth auth_type password + iniset $MAGNUM_CONF keystone_auth username magnum + iniset $MAGNUM_CONF keystone_auth password $SERVICE_PASSWORD + iniset $MAGNUM_CONF keystone_auth project_name $SERVICE_PROJECT_NAME + iniset $MAGNUM_CONF keystone_auth project_domain_id default + iniset $MAGNUM_CONF keystone_auth user_domain_id default + + # FIXME(pauloewerton): keystone_authtoken section is deprecated. Remove it + # after deprecation period. iniset $MAGNUM_CONF keystone_authtoken admin_user magnum iniset $MAGNUM_CONF keystone_authtoken admin_password $SERVICE_PASSWORD iniset $MAGNUM_CONF keystone_authtoken admin_tenant_name $SERVICE_PROJECT_NAME configure_auth_token_middleware $MAGNUM_CONF magnum $MAGNUM_AUTH_CACHE_DIR + iniset $MAGNUM_CONF keystone_auth auth_url $KEYSTONE_SERVICE_URI/v3 iniset $MAGNUM_CONF keystone_authtoken auth_uri $KEYSTONE_SERVICE_URI/v3 iniset $MAGNUM_CONF keystone_authtoken auth_version v3 diff --git a/magnum/common/clients.py b/magnum/common/clients.py index 6d3443ae77..441f5f10b0 100644 --- a/magnum/common/clients.py +++ b/magnum/common/clients.py @@ -132,13 +132,13 @@ class OpenStackClients(object): self._neutron = None def url_for(self, **kwargs): - return self.keystone().client.service_catalog.url_for(**kwargs) + return self.keystone().session.get_endpoint(**kwargs) def magnum_url(self): endpoint_type = self._get_client_option('magnum', 'endpoint_type') region_name = self._get_client_option('magnum', 'region_name') return self.url_for(service_type='container', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) def cinder_region_name(self): @@ -172,7 +172,7 @@ class OpenStackClients(object): region_name = self._get_client_option('heat', 'region_name') heatclient_version = self._get_client_option('heat', 'api_version') endpoint = self.url_for(service_type='orchestration', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) args = { @@ -199,7 +199,7 @@ class OpenStackClients(object): region_name = self._get_client_option('glance', 'region_name') glanceclient_version = self._get_client_option('glance', 'api_version') endpoint = self.url_for(service_type='image', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) args = { 'endpoint': endpoint, @@ -220,7 +220,7 @@ class OpenStackClients(object): endpoint_type = self._get_client_option('barbican', 'endpoint_type') region_name = self._get_client_option('barbican', 'region_name') endpoint = self.url_for(service_type='key-manager', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) session = self.keystone().session self._barbican = barbicanclient.Client(session=session, @@ -236,7 +236,7 @@ class OpenStackClients(object): region_name = self._get_client_option('nova', 'region_name') novaclient_version = self._get_client_option('nova', 'api_version') endpoint = self.url_for(service_type='compute', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) self._nova = novaclient.Client(novaclient_version, auth_token=self.auth_token) @@ -250,7 +250,7 @@ class OpenStackClients(object): endpoint_type = self._get_client_option('neutron', 'endpoint_type') region_name = self._get_client_option('neutron', 'region_name') endpoint = self.url_for(service_type='network', - endpoint_type=endpoint_type, + interface=endpoint_type, region_name=region_name) args = { diff --git a/magnum/common/keystone.py b/magnum/common/keystone.py index 286872a35d..72f3ec44c3 100644 --- a/magnum/common/keystone.py +++ b/magnum/common/keystone.py @@ -10,9 +10,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from keystoneclient.auth.identity import v3 +from keystoneauth1.access import access as ka_access +from keystoneauth1 import exceptions as ka_exception +from keystoneauth1.identity import access as ka_access_plugin +from keystoneauth1.identity import v3 as ka_v3 +from keystoneauth1 import loading as ka_loading +from keystoneauth1 import session as ka_session import keystoneclient.exceptions as kc_exception -from keystoneclient import session from keystoneclient.v3 import client as kc_v3 from oslo_config import cfg from oslo_log import log as logging @@ -20,9 +24,11 @@ from oslo_log import log as logging from magnum.common import exception from magnum.i18n import _ from magnum.i18n import _LE - +from magnum.i18n import _LW CONF = cfg.CONF +CFG_GROUP = 'keystone_auth' +CFG_LEGACY_GROUP = 'keystone_authtoken' LOG = logging.getLogger(__name__) trust_opts = [ @@ -39,8 +45,25 @@ trust_opts = [ 'by the trustor')) ] +legacy_session_opts = { + 'certfile': [cfg.DeprecatedOpt('certfile', CFG_LEGACY_GROUP)], + 'keyfile': [cfg.DeprecatedOpt('keyfile', CFG_LEGACY_GROUP)], + 'cafile': [cfg.DeprecatedOpt('cafile', CFG_LEGACY_GROUP)], + 'insecure': [cfg.DeprecatedOpt('insecure', CFG_LEGACY_GROUP)], + 'timeout': [cfg.DeprecatedOpt('timeout', CFG_LEGACY_GROUP)], +} + +keystone_auth_opts = (ka_loading.get_auth_common_conf_options() + + ka_loading.get_auth_plugin_conf_options('password')) + CONF.register_opts(trust_opts, group='trust') +# FIXME(pauloewerton): remove import of authtoken group and legacy options +# after deprecation period CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') +ka_loading.register_auth_conf_options(CONF, CFG_GROUP) +ka_loading.register_session_conf_options(CONF, CFG_GROUP, + deprecated_opts=legacy_session_opts) +CONF.set_default('auth_type', default='password', group=CFG_GROUP) class KeystoneClientV3(object): @@ -51,100 +74,99 @@ class KeystoneClientV3(object): self._client = None self._admin_client = None self._domain_admin_client = None + self._session = None @property def auth_url(self): - v3_auth_url = CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3') - return v3_auth_url + # FIXME(pauloewerton): auth_url should be retrieved from keystone_auth + # section by default + return CONF[CFG_LEGACY_GROUP].auth_uri.replace('v2.0', 'v3') @property def auth_token(self): - return self.client.auth_token + return self.session.get_token() @property def session(self): - return self.client.session + if self._session: + return self._session + auth = self._get_auth() + session = self._get_session(auth) + self._session = session + return session - @property - def admin_session(self): - return self.admin_client.session + def _get_session(self, auth): + session = ka_loading.load_session_from_conf_options( + CONF, CFG_GROUP, auth=auth) + return session + + def _get_auth(self): + if self.context.is_admin or self.context.trust_id: + try: + auth = ka_loading.load_auth_from_conf_options(CONF, CFG_GROUP) + except ka_exception.MissingRequiredOptions: + auth = self._get_legacy_auth() + elif self.context.auth_token_info: + access_info = ka_access.create(body=self.context.auth_token_info, + auth_token=self.context.auth_token) + auth = ka_access_plugin.AccessInfoPlugin(access_info) + elif self.context.auth_token: + auth = ka_v3.Token(auth_url=self.auth_url, + token=self.context.auth_token) + else: + LOG.error(_LE('Keystone API connection failed: no password, ' + 'trust_id or token found.')) + raise exception.AuthorizationFailure() + + return auth + + def _get_legacy_auth(self): + LOG.warning(_LW('Auth plugin and its options for service user ' + 'must be provided in [%(new)s] section. ' + 'Using values from [%(old)s] section is ' + 'deprecated.') % {'new': CFG_GROUP, + 'old': CFG_LEGACY_GROUP}) + + conf = getattr(CONF, CFG_LEGACY_GROUP) + + # FIXME(htruta, pauloewerton): Conductor layer does not have + # new v3 variables, such as project_name and project_domain_id. + # The use of admin_* variables is related to Identity API v2.0, + # which is now deprecated. We should also stop using hard-coded + # domain info, as well as variables that refer to `tenant`, + # as they are also v2 related. + auth = ka_v3.Password(auth_url=self.auth_url, + username=conf.admin_user, + password=conf.admin_password, + project_name=conf.admin_tenant_name, + project_domain_id='default', + user_domain_id='default') + return auth @property def client(self): - if self.context.is_admin: - return self.admin_client - else: - if not self._client: - self._client = self._get_ks_client() + if self._client: return self._client - - def _get_admin_credentials(self): - credentials = { - 'username': CONF.keystone_authtoken.admin_user, - 'password': CONF.keystone_authtoken.admin_password, - 'project_name': CONF.keystone_authtoken.admin_tenant_name - } - return credentials - - @property - def admin_client(self): - if not self._admin_client: - admin_credentials = self._get_admin_credentials() - self._admin_client = kc_v3.Client(auth_url=self.auth_url, - **admin_credentials) - return self._admin_client + client = kc_v3.Client(session=self.session, + trust_id=self.context.trust_id) + self._client = client + return client @property def domain_admin_client(self): if not self._domain_admin_client: - auth = v3.Password( + auth = ka_v3.Password( auth_url=self.auth_url, user_id=CONF.trust.trustee_domain_admin_id, domain_id=CONF.trust.trustee_domain_id, password=CONF.trust.trustee_domain_admin_password) - sess = session.Session(auth=auth) - self._domain_admin_client = kc_v3.Client(session=sess) + session = ka_session.Session(auth=auth) + self._domain_admin_client = kc_v3.Client(session=session) return self._domain_admin_client - @staticmethod - def _is_v2_valid(auth_token_info): - return 'access' in auth_token_info - - @staticmethod - def _is_v3_valid(auth_token_info): - return 'token' in auth_token_info - - def _get_ks_client(self): - kwargs = {'auth_url': self.auth_url, - 'endpoint': self.auth_url} - if self.context.trust_id: - kwargs.update(self._get_admin_credentials()) - kwargs['trust_id'] = self.context.trust_id - kwargs.pop('project_name') - elif self.context.auth_token_info: - kwargs['token'] = self.context.auth_token - if self._is_v2_valid(self.context.auth_token_info): - LOG.warning('Keystone v2 is deprecated.') - kwargs['auth_ref'] = self.context.auth_token_info['access'] - kwargs['auth_ref']['version'] = 'v2.0' - elif self._is_v3_valid(self.context.auth_token_info): - kwargs['auth_ref'] = self.context.auth_token_info['token'] - kwargs['auth_ref']['version'] = 'v3' - else: - LOG.error(_LE('Unknown version in auth_token_info')) - raise exception.AuthorizationFailure() - elif self.context.auth_token: - kwargs['token'] = self.context.auth_token - else: - LOG.error(_LE('Keystone v3 API conntection failed, no password ' - 'trust or auth_token')) - raise exception.AuthorizationFailure() - - return kc_v3.Client(**kwargs) - def create_trust(self, trustee_user): - trustor_user_id = self.client.auth_ref.user_id - trustor_project_id = self.client.auth_ref.project_id + trustor_user_id = self.session.get_user_id() + trustor_project_id = self.session.get_project_id() # inherit the role of the trustor, unless set CONF.trust.roles if CONF.trust.roles: diff --git a/magnum/opts.py b/magnum/opts.py index e07bf2b9a2..94d1ef54ca 100644 --- a/magnum/opts.py +++ b/magnum/opts.py @@ -59,4 +59,5 @@ def list_opts(): local_cert_manager.local_cert_manager_opts, )), ('baymodel', magnum.api.validation.baymodel_opts), + ('keystone_auth', magnum.common.keystone.keystone_auth_opts), ] diff --git a/magnum/tests/unit/common/test_clients.py b/magnum/tests/unit/common/test_clients.py index 9bd4631187..9767332270 100644 --- a/magnum/tests/unit/common/test_clients.py +++ b/magnum/tests/unit/common/test_clients.py @@ -40,11 +40,11 @@ class ClientsTest(base.BaseTestCase): @mock.patch.object(clients.OpenStackClients, 'keystone') def test_url_for(self, mock_keystone): obj = clients.OpenStackClients(None) - obj.url_for(service_type='fake_service', endpoint_type='fake_endpoint') + obj.url_for(service_type='fake_service', interface='fake_endpoint') - mock_cat = mock_keystone.return_value.client.service_catalog - mock_cat.url_for.assert_called_once_with(service_type='fake_service', - endpoint_type='fake_endpoint') + mock_endpoint = mock_keystone.return_value.session.get_endpoint + mock_endpoint.assert_called_once_with(service_type='fake_service', + interface='fake_endpoint') @mock.patch.object(clients.OpenStackClients, 'keystone') def test_magnum_url(self, mock_keystone): @@ -57,10 +57,10 @@ class ClientsTest(base.BaseTestCase): obj = clients.OpenStackClients(None) obj.magnum_url() - mock_cat = mock_keystone.return_value.client.service_catalog - mock_cat.url_for.assert_called_once_with(region_name=fake_region, - service_type='container', - endpoint_type=fake_endpoint) + mock_endpoint = mock_keystone.return_value.session.get_endpoint + mock_endpoint.assert_called_once_with(region_name=fake_region, + service_type='container', + interface=fake_endpoint) @mock.patch.object(heatclient, 'Client') @mock.patch.object(clients.OpenStackClients, 'url_for') @@ -82,7 +82,7 @@ class ClientsTest(base.BaseTestCase): auth_url='keystone_url', ca_file=None, key_file=None, password=None, insecure=False) mock_url.assert_called_once_with(service_type='orchestration', - endpoint_type='publicURL', + interface='publicURL', region_name=expected_region_name) def test_clients_heat(self): @@ -139,7 +139,7 @@ class ClientsTest(base.BaseTestCase): auth_url='keystone_url', password=None) mock_url.assert_called_once_with(service_type='image', - endpoint_type='publicURL', + interface='publicURL', region_name=expected_region_name) def test_clients_glance(self): @@ -196,7 +196,7 @@ class ClientsTest(base.BaseTestCase): mock_keystone.assert_called_once_with() mock_url.assert_called_once_with(service_type='key-manager', - endpoint_type='publicURL', + interface='publicURL', region_name=expected_region_name) def test_clients_barbican(self): @@ -251,7 +251,7 @@ class ClientsTest(base.BaseTestCase): mock_call.assert_called_once_with(cfg.CONF.nova_client.api_version, auth_token=con.auth_token) mock_url.assert_called_once_with(service_type='compute', - endpoint_type='publicURL', + interface='publicURL', region_name=expected_region_name) def test_clients_nova(self): @@ -310,7 +310,7 @@ class ClientsTest(base.BaseTestCase): auth_url='keystone_url', token='3bcc3d3a03f44e3d8377f9247b0ad155') mock_url.assert_called_once_with(service_type='network', - endpoint_type=fake_endpoint_type, + interface=fake_endpoint_type, region_name=expected_region_name) def test_clients_neutron(self): diff --git a/magnum/tests/unit/common/test_keystone.py b/magnum/tests/unit/common/test_keystone.py index 3d9aa665da..36fb068385 100644 --- a/magnum/tests/unit/common/test_keystone.py +++ b/magnum/tests/unit/common/test_keystone.py @@ -12,10 +12,13 @@ import mock from oslo_config import cfg +from oslo_config import fixture cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') +from keystoneauth1 import exceptions as ka_exception +from keystoneauth1 import identity as ka_identity import keystoneclient.exceptions as kc_exception from magnum.common import exception @@ -25,79 +28,86 @@ from magnum.tests import utils @mock.patch('keystoneclient.v3.client.Client') -class KeystoneClientTest(base.BaseTestCase): +class KeystoneClientTest(base.TestCase): def setUp(self): super(KeystoneClientTest, self).setUp() - dummy_url = 'http://server.test:5000/v2.0' + dummy_url = 'http://server.test:5000/v3' self.ctx = utils.dummy_context() self.ctx.auth_url = dummy_url self.ctx.auth_token = 'abcd1234' - cfg.CONF.set_override('auth_uri', dummy_url, - group='keystone_authtoken') - cfg.CONF.set_override('admin_user', 'magnum', - group='keystone_authtoken') - cfg.CONF.set_override('admin_password', 'verybadpass', - group='keystone_authtoken') - cfg.CONF.set_override('admin_tenant_name', 'service', - group='keystone_authtoken') + plugin = keystone.ka_loading.get_plugin_loader('password') + opts = keystone.ka_loading.get_auth_plugin_conf_options(plugin) + cfg_fixture = self.useFixture(fixture.Config()) + cfg_fixture.register_opts(opts, group=keystone.CFG_GROUP) + self.config(auth_type='password', + auth_url=dummy_url, + username='fake_user', + password='fake_pass', + project_name='fake_project', + group=keystone.CFG_GROUP) - def test_client_with_token(self, mock_ks): + self.config(auth_uri=dummy_url, + admin_user='magnum', + admin_password='varybadpass', + admin_tenant_name='service', + group=keystone.CFG_LEGACY_GROUP) + + def test_client_with_password(self, mock_ks): + self.ctx.is_admin = True ks_client = keystone.KeystoneClientV3(self.ctx) ks_client.client - self.assertIsNotNone(ks_client._client) - mock_ks.assert_called_once_with(token='abcd1234', - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3') + session = ks_client.session + auth_plugin = session.auth + mock_ks.assert_called_once_with(session=session, trust_id=None) + self.assertIsInstance(auth_plugin, ka_identity.Password) + + @mock.patch('magnum.common.keystone.ka_loading') + @mock.patch('magnum.common.keystone.ka_v3') + def test_client_with_password_legacy(self, mock_v3, mock_loading, mock_ks): + self.ctx.is_admin = True + mock_loading.load_auth_from_conf_options.side_effect = \ + ka_exception.MissingRequiredOptions(mock.MagicMock()) + ks_client = keystone.KeystoneClientV3(self.ctx) + ks_client.client + session = ks_client.session + self.assertWarnsRegex(Warning, + '[keystone_authtoken] section is deprecated') + mock_v3.Password.assert_called_once_with( + auth_url='http://server.test:5000/v3', password='varybadpass', + project_domain_id='default', project_name='service', + user_domain_id='default', username='magnum') + mock_ks.assert_called_once_with(session=session, trust_id=None) + + @mock.patch('magnum.common.keystone.ka_access') + def test_client_with_access_info(self, mock_access, mock_ks): + self.ctx.auth_token_info = mock.MagicMock() + ks_client = keystone.KeystoneClientV3(self.ctx) + ks_client.client + session = ks_client.session + auth_plugin = session.auth + mock_access.create.assert_called_once_with(body=mock.ANY, + auth_token='abcd1234') + mock_ks.assert_called_once_with(session=session, trust_id=None) + self.assertIsInstance(auth_plugin, ka_identity.access.AccessInfoPlugin) + + @mock.patch('magnum.common.keystone.ka_v3') + def test_client_with_token(self, mock_v3, mock_ks): + ks_client = keystone.KeystoneClientV3(self.ctx) + ks_client.client + session = ks_client.session + mock_v3.Token.assert_called_once_with( + auth_url='http://server.test:5000/v3', token='abcd1234') + mock_ks.assert_called_once_with(session=session, trust_id=None) def test_client_with_no_credentials(self, mock_ks): self.ctx.auth_token = None ks_client = keystone.KeystoneClientV3(self.ctx) self.assertRaises(exception.AuthorizationFailure, - ks_client._get_ks_client) - - def test_client_with_v2_auth_token_info(self, mock_ks): - self.ctx.auth_token_info = {'access': {}} - - ks_client = keystone.KeystoneClientV3(self.ctx) - ks_client.client - self.assertIsNotNone(ks_client._client) - mock_ks.assert_called_once_with(auth_ref={'version': 'v2.0'}, - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - token='abcd1234') - - def test_client_with_v3_auth_token_info(self, mock_ks): - self.ctx.auth_token_info = {'token': {}} - - ks_client = keystone.KeystoneClientV3(self.ctx) - ks_client.client - self.assertIsNotNone(ks_client._client) - mock_ks.assert_called_once_with(auth_ref={'version': 'v3'}, - auth_url='http://server.test:5000/v3', - endpoint='http://server.test:5000/v3', - token='abcd1234') - - def test_client_with_invalid_auth_token_info(self, mock_ks): - self.ctx.auth_token_info = {'not_this': 'urg'} - - ks_client = keystone.KeystoneClientV3(self.ctx) - self.assertRaises(exception.AuthorizationFailure, - ks_client._get_ks_client) - - def test_client_with_is_admin(self, mock_ks): - self.ctx.is_admin = True - ks_client = keystone.KeystoneClientV3(self.ctx) - ks_client.client - - self.assertIsNone(ks_client._client) - self.assertIsNotNone(ks_client._admin_client) - mock_ks.assert_called_once_with(auth_url='http://server.test:5000/v3', - username='magnum', - password='verybadpass', - project_name='service') + ks_client._get_auth) + mock_ks.assert_not_called() def test_delete_trust(self, mock_ks): mock_ks.return_value.trusts.delete.return_value = None @@ -111,9 +121,10 @@ class KeystoneClientTest(base.BaseTestCase): ks_client = keystone.KeystoneClientV3(self.ctx) self.assertIsNone(ks_client.delete_trust(trust_id='atrust123')) - def test_create_trust_with_all_roles(self, mock_ks): - mock_ks.return_value.auth_ref.user_id = '123456' - mock_ks.return_value.auth_ref.project_id = '654321' + @mock.patch('magnum.common.keystone.ka_session.Session') + def test_create_trust_with_all_roles(self, mock_session, mock_ks): + mock_session.return_value.get_user_id.return_value = '123456' + mock_session.return_value.get_project_id.return_value = '654321' self.ctx.roles = ['role1', 'role2'] ks_client = keystone.KeystoneClientV3(self.ctx) @@ -125,9 +136,10 @@ class KeystoneClientTest(base.BaseTestCase): trustee_user='888888', role_names=['role1', 'role2'], impersonation=True) - def test_create_trust_with_limit_roles(self, mock_ks): - mock_ks.return_value.auth_ref.user_id = '123456' - mock_ks.return_value.auth_ref.project_id = '654321' + @mock.patch('magnum.common.keystone.ka_session.Session') + def test_create_trust_with_limit_roles(self, mock_session, mock_ks): + mock_session.return_value.get_user_id.return_value = '123456' + mock_session.return_value.get_project_id.return_value = '654321' self.ctx.roles = ['role1', 'role2'] ks_client = keystone.KeystoneClientV3(self.ctx) diff --git a/requirements.txt b/requirements.txt index 2653251fc1..7d41a40b4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ eventlet!=0.18.3,>=0.18.2 # MIT greenlet>=0.3.2 # MIT iso8601>=0.1.9 # MIT jsonpatch>=1.1 # BSD +keystoneauth1>=2.1.0 # Apache-2.0 keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0 netaddr!=0.7.16,>=0.7.12 # BSD oslo.concurrency>=3.5.0 # Apache-2.0