Fix authentication of Nova client

Blazar authenticates Nova requests with trust-scoped tokens. Sometime
during the Ocata cycle these requests started failing (possibly due to
stricter validation in Keystone) with the error:

    BadRequest: Invalid input for field 'identity/password/user/password': None is not of type 'string' (HTTP 400)

This commit changes how the Nova client is configured to use the
token_endpoint authentication plugin combined with endpoint_override,
which allows to communicate with the Nova endpoint without extra
requests to Keystone. This is necessary between trust-scoped tokens
cannot re-authenticate with Keystone, which happens with other
authentication plugins.

Change-Id: Ibb6782140f41aea5e539e11f2618b3af2628fc4c
Closes-Bug: #1660564
This commit is contained in:
Pierre Riteau 2017-02-05 19:23:58 +00:00
parent 98d4c7ddf4
commit 81b85a11af
2 changed files with 36 additions and 56 deletions

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from keystoneauth1 import session
from keystoneauth1 import token_endpoint
from novaclient import client as nova_client from novaclient import client as nova_client
from climate import context from climate import context
@ -32,44 +34,41 @@ class TestCNClient(tests.TestCase):
self.ctx = self.patch(self.context, 'current') self.ctx = self.patch(self.context, 'current')
self.client = self.patch(self.n_client, 'Client') self.client = self.patch(self.n_client, 'Client')
self.patch(self.base, 'url_for').return_value = 'http://fake.com/' self.auth = self.patch(token_endpoint, 'Token')
self.session = self.patch(session, 'Session')
self.url = 'http://fake.com/'
self.patch(self.base, 'url_for').return_value = self.url
self.version = '2' self.version = '2'
self.username = 'fake_user'
self.api_key = self.ctx().auth_token
self.project_id = self.ctx().project_id
self.auth_url = 'fake_auth'
self.mgmt_url = 'fake_mgmt'
def test_client_from_kwargs(self): def test_client_from_kwargs(self):
self.ctx.side_effect = RuntimeError self.ctx.side_effect = RuntimeError
self.auth_token = 'fake_token'
self.endpoint = 'fake_endpoint'
kwargs = {'version': self.version, kwargs = {'version': self.version,
'username': self.username, 'endpoint_override': self.endpoint,
'api_key': self.api_key, 'auth_token': self.auth_token}
'project_id': self.project_id,
'auth_url': self.auth_url,
'mgmt_url': self.mgmt_url}
self.nova.ClimateNovaClient(**kwargs) self.nova.ClimateNovaClient(**kwargs)
self.auth.assert_called_once_with(self.endpoint, self.auth_token)
self.session.assert_called_once_with(auth=self.auth.return_value)
self.client.assert_called_once_with(version=self.version, self.client.assert_called_once_with(version=self.version,
username=self.username, endpoint_override=self.endpoint,
api_key=self.api_key, session=self.session.return_value)
project_id=self.project_id,
auth_url=self.auth_url)
def test_client_from_ctx(self): def test_client_from_ctx(self):
kwargs = {'version': self.version} kwargs = {'version': self.version}
self.nova.ClimateNovaClient(**kwargs) self.nova.ClimateNovaClient(**kwargs)
self.auth.assert_called_once_with(self.url,
self.ctx().auth_token)
self.session.assert_called_once_with(auth=self.auth.return_value)
self.client.assert_called_once_with(version=self.version, self.client.assert_called_once_with(version=self.version,
username=self.ctx().user_name, endpoint_override=self.url,
api_key=None, session=self.session.return_value)
project_id=self.ctx().project_id,
auth_url='http://fake.com/')
def test_getattr(self): def test_getattr(self):
# TODO(n.s.): Will be done as soon as pypi package will be updated # TODO(n.s.): Will be done as soon as pypi package will be updated

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from keystoneauth1 import session
from keystoneauth1 import token_endpoint
from novaclient import client as nova_client from novaclient import client as nova_client
from novaclient import exceptions as nova_exception from novaclient import exceptions as nova_exception
from novaclient.v2 import servers from novaclient.v2 import servers
@ -50,55 +52,37 @@ class ClimateNovaClient(object):
:param version: service client version which we will use :param version: service client version which we will use
:type version: str :type version: str
:param username: username
:type username: str
:param api_key: password
:type api_key: str
:param auth_token: keystone auth token :param auth_token: keystone auth token
:type auth_token: str :type auth_token: str
:param project_id: project_id :param endpoint_override: endpoint url which we will use
:type api_key: str :type endpoint_override: str
:param auth_url: auth_url
:type auth_url: str
:param mgmt_url: management url
:type mgmt_url: str
""" """
ctx = kwargs.pop('ctx', None) ctx = kwargs.pop('ctx', None)
auth_token = kwargs.pop('auth_token', None) auth_token = kwargs.pop('auth_token', None)
mgmt_url = kwargs.pop('mgmt_url', None) endpoint_override = kwargs.pop('endpoint_override', None)
version = kwargs.pop('version', cfg.CONF.nova_client_version)
if ctx is None: if ctx is None:
try: try:
ctx = context.current() ctx = context.current()
except RuntimeError: except RuntimeError:
pass pass
kwargs.setdefault('version', cfg.CONF.nova_client_version)
if ctx is not None: if ctx is not None:
kwargs.setdefault('username', ctx.user_name)
kwargs.setdefault('api_key', None)
kwargs.setdefault('project_id', ctx.project_id)
kwargs.setdefault('auth_url', base.url_for(
ctx.service_catalog, CONF.identity_service))
auth_token = auth_token or ctx.auth_token auth_token = auth_token or ctx.auth_token
mgmt_url = mgmt_url or base.url_for(ctx.service_catalog, endpoint_override = endpoint_override or \
CONF.compute_service) base.url_for(ctx.service_catalog,
if not kwargs.get('auth_url', None): CONF.compute_service)
# NOTE(scroiset): novaclient v2.17.0 support only Identity API v2.0
auth_url = "%s://%s:%s/v2.0" % (CONF.os_auth_protocol,
CONF.os_auth_host,
CONF.os_auth_port)
kwargs['auth_url'] = auth_url
auth = token_endpoint.Token(endpoint_override,
auth_token)
sess = session.Session(auth=auth)
kwargs.setdefault('endpoint_override', endpoint_override)
kwargs.setdefault('session', sess)
kwargs.setdefault('version', version)
self.nova = nova_client.Client(**kwargs) self.nova = nova_client.Client(**kwargs)
self.nova.client.auth_token = auth_token
self.nova.client.management_url = mgmt_url
self.nova.servers = ServerManager(self.nova) self.nova.servers = ServerManager(self.nova)
@ -136,8 +120,5 @@ class NovaClientWrapper(object):
@property @property
def nova(self): def nova(self):
ctx = context.current() ctx = context.current()
nova = ClimateNovaClient(username=ctx.user_name, nova = ClimateNovaClient(ctx=ctx)
api_key=None,
project_id=ctx.project_id,
ctx=ctx)
return nova return nova