diff --git a/ironic/api/acl.py b/ironic/api/acl.py index 453b67b465..b9bf8fc0d4 100644 --- a/ironic/api/acl.py +++ b/ironic/api/acl.py @@ -36,9 +36,6 @@ def register_opts(conf): keystone_auth_token.CONF = conf -register_opts(cfg.CONF) - - def install(app, conf, public_routes): """Install ACL check on application. @@ -49,6 +46,7 @@ def install(app, conf, public_routes): :return: The same WSGI application with ACL installed. """ + register_opts(cfg.CONF) keystone_config = dict(conf.get(OPT_GROUP_NAME)) return auth_token.AuthTokenMiddleware(app, conf=keystone_config, diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 2f936f76f6..1e9a03557c 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -360,6 +360,19 @@ class InvalidImageRef(Invalid): message = "Invalid image href %(image_href)s." +class CatalogUnauthorized(IronicException): + message = _("Unauthorised for keystone service catalog.") + + +class CatalogFailure(IronicException): + pass + + +class CatalogNotFound(IronicException): + message = _("Attr %(attr)s with value %(value)s not found in keystone " + "service catalog.") + + class ServiceUnavailable(IronicException): message = "Connection failed" diff --git a/ironic/common/keystone.py b/ironic/common/keystone.py new file mode 100644 index 0000000000..273a6d0dc0 --- /dev/null +++ b/ironic/common/keystone.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 +# +# 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. + +from keystoneclient import exceptions as ksexception +from oslo.config import cfg +from six.moves.urllib import parse + +from ironic.api import acl +from ironic.common import exception + +CONF = cfg.CONF +acl.register_opts(CONF) + + +def get_service_url(attr='name', filter_value='ironic', + service_type='baremetal', endpoint_type='internal'): + """Wrapper for get service url from keystone service catalog.""" + auth_url = CONF.keystone_authtoken.auth_uri or '' + api_v3 = CONF.keystone_authtoken.auth_version == 'v3.0' or \ + 'v3' in parse.urlparse(auth_url).path + + if api_v3: + from keystoneclient.v3 import client + else: + from keystoneclient.v2_0 import client + + try: + ksclient = client.Client(username=CONF.keystone_authtoken.admin_user, + password=CONF.keystone_authtoken.admin_password, + tenant_name=CONF.keystone_authtoken.admin_tenant_name, + auth_url=auth_url) + except ksexception.Unauthorized: + raise exception.CatalogUnauthorized + + except ksexception.AuthorizationFailure as err: + raise exception.CatalogFailure(_('Could not perform authorization ' + 'process for service catalog: %s') + % err) + + if not ksclient.has_service_catalog(): + raise exception.CatalogFailure(_('No keystone service catalog loaded')) + + try: + endpoint = ksclient.service_catalog.url_for(attr=attr, + filter_value=filter_value, + service_type=service_type, + endpoint_type=endpoint_type) + except ksexception.EndpointNotFound: + raise exception.CatalogNotFound(attr=attr, value=filter_value) + + return endpoint diff --git a/ironic/tests/test_keystone.py b/ironic/tests/test_keystone.py new file mode 100644 index 0000000000..75fb41020f --- /dev/null +++ b/ironic/tests/test_keystone.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# -*- encoding: utf-8 -*- +# +# 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 fixtures +from keystoneclient import exceptions as ksexception + +from ironic.common import exception +from ironic.common import keystone +from ironic.tests import base + + +class KeystoneTestCase(base.TestCase): + + def setUp(self): + super(KeystoneTestCase, self).setUp() + self.config(group='keystone_authtoken', + auth_uri='http://127.0.0.1:9898/', + admin_user='fake', admin_password='fake', + admin_tenant_name='fake') + + def test_failure_authorization(self): + self.assertRaises(exception.CatalogFailure, keystone.get_service_url) + + def test_get_url(self): + fake_url = 'http://127.0.0.1:6385' + + class _fake_catalog: + def url_for(self, **kwargs): + return fake_url + + class _fake_client: + def __init__(self, **kwargs): + self.service_catalog = _fake_catalog() + + def has_service_catalog(self): + return True + + self.useFixture(fixtures.MonkeyPatch( + 'keystoneclient.v2_0.client.Client', + _fake_client)) + + res = keystone.get_service_url() + self.assertEqual(res, fake_url) + + def test_url_not_found(self): + + class _fake_catalog: + def url_for(self, **kwargs): + raise ksexception.EndpointNotFound + + class _fake_client: + def __init__(self, **kwargs): + self.service_catalog = _fake_catalog() + + def has_service_catalog(self): + return True + + self.useFixture(fixtures.MonkeyPatch( + 'keystoneclient.v2_0.client.Client', + _fake_client)) + + self.assertRaises(exception.CatalogNotFound, keystone.get_service_url) + + def test_no_catalog(self): + + class _fake_client: + def __init__(self, **kwargs): + pass + + def has_service_catalog(self): + return False + + self.useFixture(fixtures.MonkeyPatch( + 'keystoneclient.v2_0.client.Client', + _fake_client)) + + self.assertRaises(exception.CatalogFailure, keystone.get_service_url) + + def test_unauthorized(self): + + class _fake_client: + def __init__(self, **kwargs): + raise ksexception.Unauthorized + + self.useFixture(fixtures.MonkeyPatch( + 'keystoneclient.v2_0.client.Client', + _fake_client)) + + self.assertRaises(exception.CatalogUnauthorized, + keystone.get_service_url)