diff --git a/README.rst b/README.rst index a52532f6..eed5652d 100644 --- a/README.rst +++ b/README.rst @@ -213,7 +213,6 @@ Legacy OpenStack release notice EC2 API supports Havana, Icehouse, Juno with additional limitations: - Instance related: - rootDeviceName Instance property - kernelId Instance property @@ -237,7 +236,8 @@ EC2 API supports Nova client (>=2.16.0) with no microversion support. Additional limitations are the same, except network interfaces' deleteOnTermination. -Preferred way to run EC2 API in these releases is to run it in virtual environment: + +Preferred way to run EC2 API in older releases is to run it in virtual environment: - create virtual environment by running command 'python tools/install_venv.py' - run install inside venv 'tools/with_venv.sh ./install.sh' - and then you need to run EC2 API services: 'ec2-api', 'ec2-api-metadata' diff --git a/ec2api/api/__init__.py b/ec2api/api/__init__.py index 59b70eb1..63c2cc20 100644 --- a/ec2api/api/__init__.py +++ b/ec2api/api/__init__.py @@ -228,11 +228,21 @@ class EC2KeystoneAuth(wsgi.Middleware): result = response.json() try: - token_id = result['access']['token']['id'] - user_id = result['access']['user']['id'] - project_id = result['access']['token']['tenant']['id'] - user_name = result['access']['user'].get('name') - project_name = result['access']['token']['tenant'].get('name') + if 'token' in result: + # NOTE(andrey-mp): response from keystone v3 + token_id = response.headers['x-subject-token'] + user_id = result['token']['user']['id'] + project_id = result['token']['project']['id'] + user_name = result['token']['user'].get('name') + project_name = result['token']['project'].get('name') + catalog = result['token']['catalog'] + else: + token_id = result['access']['token']['id'] + user_id = result['access']['user']['id'] + project_id = result['access']['token']['tenant']['id'] + user_name = result['access']['user'].get('name') + project_name = result['access']['token']['tenant'].get('name') + catalog = result['access']['serviceCatalog'] except (AttributeError, KeyError): LOG.exception(_("Keystone failure")) msg = _("Failure communicating with keystone") @@ -244,7 +254,6 @@ class EC2KeystoneAuth(wsgi.Middleware): remote_address = req.headers.get('X-Forwarded-For', remote_address) - catalog = result['access']['serviceCatalog'] ctxt = context.RequestContext(user_id, project_id, request_id=request_id, user_name=user_name, diff --git a/ec2api/api/clients.py b/ec2api/api/clients.py index a91f98ce..4d4b8075 100644 --- a/ec2api/api/clients.py +++ b/ec2api/api/clients.py @@ -13,7 +13,6 @@ # limitations under the License. -from keystoneclient.v2_0 import client as kc from novaclient import client as novaclient from novaclient import exceptions as nova_exception from oslo_config import cfg @@ -135,13 +134,13 @@ def cinder(context): def keystone(context): - _keystone = kc.Client( + keystone_client_class = ec2_context.get_keystone_client_class() + return keystone_client_class( token=context.auth_token, + project_id=context.project_id, tenant_id=context.project_id, auth_url=CONF.keystone_url) - return _keystone - def nova_cert(context): _cert_api = _rpcapi_CertAPI(context) @@ -162,6 +161,9 @@ def _url_for(context, **kwargs): for endpoint in service['endpoints']: if 'publicURL' in endpoint: return endpoint['publicURL'] + elif endpoint.get('interface') == 'public': + # NOTE(andrey-mp): keystone v3 + return endpoint['url'] else: return None diff --git a/ec2api/context.py b/ec2api/context.py index aeeacd44..bac7d6cf 100644 --- a/ec2api/context.py +++ b/ec2api/context.py @@ -16,7 +16,9 @@ import uuid -from keystoneclient.v2_0 import client as keystone_client +from keystoneclient import client as keystone_client +from keystoneclient.v2_0 import client as keystone_client_v2 +from keystoneclient.v3 import client as keystone_client_v3 from oslo_config import cfg from oslo_context import context from oslo_log import log as logging @@ -35,6 +37,8 @@ ec2_opts = [ secret=True), cfg.StrOpt('admin_tenant_name', help=_("Admin tenant name")), + # TODO(andrey-mp): keystone v3 allows to pass domain_name + # or domain_id to auth. This code should support this feature. ] CONF = cfg.CONF @@ -143,15 +147,33 @@ def is_user_context(context): return True +_keystone_client_class = None + + +def get_keystone_client_class(): + global _keystone_client_class + if _keystone_client_class is None: + keystone = keystone_client.Client(auth_url=CONF.keystone_url) + if isinstance(keystone, keystone_client_v2.Client): + _keystone_client_class = keystone_client_v2.Client + elif isinstance(keystone, keystone_client_v3.Client): + _keystone_client_class = keystone_client_v3.Client + else: + raise exception.EC2KeystoneDiscoverFailure() + return _keystone_client_class + + def get_os_admin_context(): """Create a context to interact with OpenStack as an administrator.""" current_context = context.get_current() if (current_context and current_context.is_os_admin): return current_context # TODO(ft): make an authentification token reusable - keystone = keystone_client.Client( + keystone_client_class = get_keystone_client_class() + keystone = keystone_client_class( username=CONF.admin_user, password=CONF.admin_password, + project_name=CONF.admin_tenant_name, tenant_name=CONF.admin_tenant_name, auth_url=CONF.keystone_url, ) diff --git a/ec2api/exception.py b/ec2api/exception.py index fa32b15b..fbf14991 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -96,6 +96,10 @@ class EC2APIPasteAppNotFound(EC2APIException): msg_fmt = _("Could not load paste app '%(name)s' from %(path)s") +class EC2KeystoneDiscoverFailure(EC2APIException): + msg_fmt = _("Could not discover keystone versions.") + + # Internal ec2api metadata exceptions class EC2MetadataException(EC2APIException): diff --git a/ec2api/tests/unit/test_clients.py b/ec2api/tests/unit/test_clients.py index c3303bdf..7150190e 100644 --- a/ec2api/tests/unit/test_clients.py +++ b/ec2api/tests/unit/test_clients.py @@ -123,13 +123,14 @@ class ClientsTestCase(test_base.BaseTestCase): self.assertEqual('fake_token', res.client.auth_token) self.assertEqual('cinder_url', res.client.management_url) - @mock.patch('keystoneclient.v2_0.client.Client') - def test_keystone(self, keystone): + @mock.patch('ec2api.context.get_keystone_client_class', + return_value=mock.Mock(return_value=mock.Mock())) + def test_keystone(self, keystone_client_class): context = mock.NonCallableMock( auth_token='fake_token', project_id='fake_project') res = clients.keystone(context) - self.assertEqual(keystone.return_value, res) - keystone.assert_called_with( + self.assertEqual(keystone_client_class.return_value.return_value, res) + keystone_client_class.return_value.assert_called_with( auth_url='keystone_url', token='fake_token', - tenant_id='fake_project') + tenant_id='fake_project', project_id='fake_project') diff --git a/ec2api/tests/unit/test_context.py b/ec2api/tests/unit/test_context.py index c31b73f9..0a8ec18a 100644 --- a/ec2api/tests/unit/test_context.py +++ b/ec2api/tests/unit/test_context.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystoneclient.v2_0 import client as keystone_client_v2 +from keystoneclient.v3 import client as keystone_client_v3 import mock from oslo_config import cfg from oslo_config import fixture as config_fixture from oslotest import base as test_base from ec2api import context as ec2_context +from ec2api import exception cfg.CONF.import_opt('keystone_url', 'ec2api.api') @@ -35,10 +38,12 @@ class ContextTestCase(test_base.BaseTestCase): def test_get_os_admin_context(self, keystone): service_catalog = mock.Mock() service_catalog.get_data.return_value = 'fake_service_catalog' - keystone.return_value = mock.Mock(auth_user_id='fake_user_id', - auth_tenant_id='fake_project_id', - auth_token='fake_token', - service_catalog=service_catalog) + ec2_context._keystone_client_class = mock.Mock( + return_value=mock.Mock( + auth_user_id='fake_user_id', + auth_tenant_id='fake_project_id', + auth_token='fake_token', + service_catalog=service_catalog)) context = ec2_context.get_os_admin_context() self.assertEqual('fake_user_id', context.user_id) self.assertEqual('fake_project_id', context.project_id) @@ -46,13 +51,35 @@ class ContextTestCase(test_base.BaseTestCase): self.assertEqual('fake_service_catalog', context.service_catalog) self.assertTrue(context.is_os_admin) conf = cfg.CONF - keystone.assert_called_once_with( - username=conf.admin_user, - password=conf.admin_password, - tenant_name=conf.admin_tenant_name, - auth_url=conf.keystone_url) + ec2_context._keystone_client_class.assert_called_once_with( + username=conf.admin_user, + password=conf.admin_password, + tenant_name=conf.admin_tenant_name, + project_name=conf.admin_tenant_name, + auth_url=conf.keystone_url) service_catalog.get_data.assert_called_once_with() keystone.reset_mock() self.assertEqual(context, ec2_context.get_os_admin_context()) self.assertFalse(keystone.called) + + @mock.patch('keystoneclient.client.Client') + def test_get_keystone_client_class(self, client): + client.return_value = mock.MagicMock(spec=keystone_client_v2.Client) + ec2_context._keystone_client_class = None + client_class = ec2_context.get_keystone_client_class() + client.assert_called_once_with(auth_url='http://localhost:5000/v2.0') + self.assertEqual(keystone_client_v2.Client, client_class) + client.reset_mock() + + client.return_value = mock.MagicMock(spec=keystone_client_v3.Client) + ec2_context._keystone_client_class = None + client_class = ec2_context.get_keystone_client_class() + client.assert_called_once_with(auth_url='http://localhost:5000/v2.0') + self.assertEqual(keystone_client_v3.Client, client_class) + client.reset_mock() + + client.return_value = mock.MagicMock() + ec2_context._keystone_client_class = None + self.assertRaises(exception.EC2KeystoneDiscoverFailure, + ec2_context.get_keystone_client_class) diff --git a/ec2api/tests/unit/test_metadata.py b/ec2api/tests/unit/test_metadata.py index 6455b2b7..f4964a95 100644 --- a/ec2api/tests/unit/test_metadata.py +++ b/ec2api/tests/unit/test_metadata.py @@ -333,17 +333,19 @@ class ProxyTestCase(test_base.BaseTestCase): self.handler._unpack_request_attributes(req) self.assertEqual(1, constant_time_compare.call_count) - @mock.patch('keystoneclient.v2_0.client.Client') + @mock.patch('ec2api.context.get_keystone_client_class') @mock.patch('novaclient.client.Client') @mock.patch('ec2api.db.api.IMPL') @mock.patch('ec2api.metadata.api.instance_api') - def test_get_metadata(self, instance_api, db_api, nova, keystone): + def test_get_metadata(self, instance_api, db_api, nova, + keystone_client_class): service_catalog = mock.MagicMock() service_catalog.get_data.return_value = [] - keystone.return_value = mock.Mock(auth_user_id='fake_user_id', - auth_tenant_id='fake_project_id', - auth_token='fake_token', - service_catalog=service_catalog) + keystone_client_class.return_value.return_value = mock.Mock( + auth_user_id='fake_user_id', + auth_tenant_id='fake_project_id', + auth_token='fake_token', + service_catalog=service_catalog) nova.return_value.fixed_ips.get.return_value = ( mock.Mock(hostname='fake_name')) nova.return_value.servers.list.return_value = [