monasca-agent/monasca_agent/common/keystone.py

340 lines
13 KiB
Python

# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 Fujitsu LIMITED
# 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 logging
from keystoneauth1 import identity
from keystoneauth1 import session
from keystoneclient import discover
import six
from monasca_agent.common import singleton
from monasca_agent import version as ma_version
LOG = logging.getLogger(__name__)
_DEFAULT_SERVICE_TYPE = 'monitoring'
_DEFAULT_ENDPOINT_TYPE = 'public'
def _sanitize_args(data):
"""Removes keys for which value is None.
:param data: dictionary with data
:type data: dict
:return: cleaned data
:rtype: dict
"""
return {k: v for k, v in data.items() if v is not None}
def get_session(**kwargs):
"""Creates new keystone session.
Method uses :py:class:`keystoneauth1.identity.Password`
abstracting from underlying Keystone version
This method is capable of creating a session regardless of
Keystone version (either v2 or v3). However if:
- using **Keystone v2** following arguments [domain_id, domain_name,
project_domain_id and project_domain_name] should not be set. Keystone V2
does not support authentication with domain scope.
- using **Keystone v2** following arguments are prohibited:
[user_domain_id, user_domain_name]
- using **Keystone v3** be careful with the scope of authentication.
For more details about scopes refer to identity_tokens_ and v3_identity_
.. _v3_api: https://developer.openstack.org/api-ref/identity/v3/index.html?expanded=token-authe
ntication-with-scoped-authorization-detail
.. _identity_tokens: https://docs.openstack.org/admin-guide/identity-tokens.html
In overall:
- for **Keystone V2** following arguments are allowed:
[auth_url, user_id, username, password, trust_id, tenant_name,
tenant_id, project_name, project_id].
* for **Keystone V3** following argumenta are allowed:
[auth_url, user_id, username, password, user_domain_id, user_domain_name,
trust_id, project_id, project_name, project_domain_id,
project_domain_name, domain_id, domain_name, tenant_id, tenant_name]
However, note that project_id and project_name will override tenant_id
and tenant_name, as in::
>>> project_id = project_id or tenant_id
>>> project_name = project_name or tenant_name
Arguments tenant_id and tenant_name are kept for sake of
backward compatibility between two versions of Keystone.
Note:
Keystone version is resolved on the runtime
by keystoneauth1 library
:param string auth_url: URL of keystone service.
:param string username: Username for authentication.
:param string password: Password for authentication.
:param string user_id: User ID for authentication.
:param string user_domain_id: User's domain ID for authentication
(replaced by default_domain_if if set)
:param string user_domain_name: User's domain name for authentication
(replaced by default_domain_name if set)
:param string project_id: Project ID for authentication
:param string project_name: Project Name for authentication
:param string project_domain_id: Project Domain ID for authentication
:param string project_domain_name: Project Domain Name for authentication
:param string tenant_id: Tenant ID for authentication
(replaced by project_id if set)
:param string tenant_name: Tenant Name for authentication
(replaced by project_name if set)
:param string domain_id: Domain ID for authentication.
:param string domain_name: Domain name for authentication
:param string trust_id: Trust ID for authentication.
:param string default_domain_id: Default domain ID for authentication.
:param string default_domain_name: Default domain name for authentication
:param float keystone_timeout: A timeout to pass to requests. This should be a
numerical value indicating some amount (or fraction)
of seconds or 0 for no timeout. (optional, defaults
to 0)
:param verify: The verification arguments to pass to requests. These are of
the same form as requests expects, so True or False to
verify (or not) against system certificates or a path to a
bundle or CA certs to check against or None for requests to
attempt to locate and use certificates. (optional, defaults
to True)
:param bool reauthenticate: Should reauthenticate if token expires
(optional, defaults to True)
:return: session instance
:rtype: keystoneauth1.session.Session
"""
LOG.debug('Initializing keystone session using generic password')
auth = identity.Password(
auth_url=kwargs.get('auth_url', None),
username=kwargs.get('username', None),
password=kwargs.get('password', None),
user_id=kwargs.get('user_id', None),
user_domain_id=kwargs.get('user_domain_id', None),
user_domain_name=kwargs.get('user_domain_name', None),
project_id=kwargs.get('project_id', None),
project_name=kwargs.get('project_name', None),
project_domain_id=kwargs.get('project_domain_id', None),
project_domain_name=kwargs.get('project_domain_name', None),
tenant_id=kwargs.get('tenant_id', None),
tenant_name=kwargs.get('tenant_name', None),
domain_id=kwargs.get('domain_id', None),
domain_name=kwargs.get('domain_name', None),
trust_id=kwargs.get('trust_id', None),
default_domain_id=kwargs.get('default_domain_id', None),
default_domain_name=kwargs.get('default_domain_name', None),
reauthenticate=kwargs.get('reauthenticate', True)
)
sess = session.Session(auth=auth,
app_name='monasca-agent',
app_version=ma_version.version_string,
user_agent='monasca-agent',
timeout=kwargs.get('keystone_timeout', None),
verify=kwargs.get('verify', True))
return sess
def get_client(**kwargs):
"""Creates new keystone client.
Initializes new keystone client.
Method does not assume what version of keystone is used.
That responsibility is delegated to
:py:class:`keystoneauth1.discover.Discover`.
Version of the keystone will be the newest one available.
There are two ways to call this method:
using existing session object (:py:class:`keystoneauth1.session.Session`::
>>> s = session.Session(**args)
>>> c = get_client(session=s)
initializing new keystone client from credentials::
>>> c = get_client({'username':'mini-mon', 'password':'test', ...})
:param kwargs: list of arguments passed to method
:type kwargs: dict
:return: keystone client instance
:rtype: Union[keystoneclient.v3.client.Client,
keystoneclient.v2_0.client.Client]
"""
if 'session' not in kwargs:
LOG.debug('Initializing fresh keystone client')
sess = get_session(**kwargs)
else:
LOG.debug('Initializing keystone client from existing session')
sess = kwargs.get('session')
disc = discover.Discover(session=sess)
LOG.debug('Available keystone versions are %s' % disc.version_data())
ks = disc.create_client(**kwargs)
ks.auth_ref = sess.auth.get_auth_ref(session=sess)
LOG.info('Using keystone version %s', ks.version)
return ks
def get_args(config):
"""Utility to extract keystone args from agent's config.
Method retrieves all keystone related settings, from
agent's configuration, that are actually set.
:param config: agent's config
:type config: dict
:returns: cleaned args
:rtype: dict
"""
raw_args = {
'auth_url': config.get('keystone_url', None),
'username': config.get('username', None),
'password': config.get('password', None),
'user_id': config.get('user_id', None),
'user_domain_id': config.get('user_domain_id', None),
'user_domain_name': config.get('user_domain_name', None),
'project_id': config.get('project_id', None),
'project_name': config.get('project_name', None),
'project_domain_name': config.get('project_domain_name', None),
'project_domain_id': config.get('project_domain_id', None),
'domain_id': config.get('domain_id', None),
'domain_name': config.get('domain_name', None),
'tenant_id': config.get('tenant_id', None),
'tenant_name': config.get('tenant_name', None),
'trust_id': config.get('trust_id', None),
'default_domain_id': config.get('default_domain_id', None),
'default_domain_name': config.get('default_domain_name', None),
'url': config.get('url', None), # hardcoded monasca-api url
'service_type': config.get('service_type', _DEFAULT_SERVICE_TYPE),
'endpoint_type': config.get('endpoint_type', _DEFAULT_ENDPOINT_TYPE),
'region_name': config.get('region_name', None),
'keystone_timeout': config.get('keystone_timeout', None),
'verify': False if config.get('insecure') else config.get('ca_file', None),
'reauthenticate': config.get('reauthenticate', True)
}
clean_args = _sanitize_args(raw_args)
LOG.debug('Removed %d keys that did not present values in configuration',
len(raw_args) - len(clean_args))
return clean_args
@six.add_metaclass(singleton.Singleton)
class Keystone(object):
def __init__(self, config):
self._config = get_args(config)
self._keystone_client = None
def _init_client(self):
"""Get a new keystone client object.
For more details see:
- :py:func:`monasca_agent.common.keystone.get_session(**args)`
- :py:func:`monasca_agent.common.keystone.get_client(**args)`
Note:
This method initializes client only once on
behalf of its own
:return: keystone client instance
:rtype: Union[keystoneclient.v3.client.Client,
keystoneclient.v2_0.client.Client]
"""
if self._keystone_client:
LOG.debug('Keystone client is already initialized')
return self._keystone_client
ks = get_client(**self._config)
self._keystone_client = ks
return ks
def get_monasca_url(self):
"""Retrieves monasca endpoint url.
monasca endpoint url can be retrieved from two locations:
* agent configuration (value must be present under api.url key)
* keystone catalog (requires settings api.service_type,
api.endpoint_type and api.region_name)
First method tries low-cost approach: checking if url is available
in configuration file. If not, it moves to querying the keystone
catalog
:return: monasca endpoint url
:rtype: basestring
"""
if self._config.get('url', None):
endpoint = self._config.get('url')
LOG.debug('Using monasca-api url %s from configuration' % endpoint)
else:
# NOTE(trebskit) no need to sanitize these values here
# as we're using already local (clean) copy
args = {
'service_type': self._config.get('service_type'),
'interface': self._config.get('endpoint_type'),
'region_name': self._config.get('region_name', None) # that one has no default
}
catalog = self._init_client().auth_ref.service_catalog
endpoint = catalog.url_for(**args)
LOG.debug('Using monasca-api url %s from catalog[%s]'
% (endpoint, args))
return endpoint
def get_token(self):
"""Validate token is project scoped and return it if it is
project_id and auth_token were fetched when keystone client was created
"""
return self._init_client().auth_token
def refresh_token(self):
"""Gets a new keystone client object and token
This method should be called if the token has expired
"""
self._keystone_client = None
return self.get_token()
def get_session(self):
"""Returns session of this client.
:return: session instance
:rtype: keystoneauth1.session.Session
"""
return self._init_client().session