summaryrefslogtreecommitdiff
path: root/heat/engine/clients
diff options
context:
space:
mode:
authorrabi <ramishra@redhat.com>2016-05-24 14:16:06 +0530
committerrabi <ramishra@redhat.com>2016-06-01 16:27:01 +0530
commite0e92b9d6de8a2900a5faf07c4c9c86404db5601 (patch)
treefe656d99b8d63dd8b0e790305fd90ebfbba429dd /heat/engine/clients
parent2c05a6132854166e1f799927954715f554968247 (diff)
downloadheat-e0e92b9d6de8a2900a5faf07c4c9c86404db5601.tar.gz
Move heat_keystoneclient to clients package
This moves the heat_keystoneclient wrapper to heat.engine.clients package. Change-Id: I39636abb946a7608014145d9edca5297d9f929d0 Related-Bug: #1554533
Diffstat (limited to 'heat/engine/clients')
-rw-r--r--heat/engine/clients/os/keystone/__init__.py (renamed from heat/engine/clients/os/keystone.py)65
-rw-r--r--heat/engine/clients/os/keystone/heat_keystoneclient.py571
-rw-r--r--heat/engine/clients/os/keystone/keystone_constraints.py77
3 files changed, 649 insertions, 64 deletions
diff --git a/heat/engine/clients/os/keystone.py b/heat/engine/clients/os/keystone/__init__.py
index 7302e27e4..52b160a22 100644
--- a/heat/engine/clients/os/keystone.py
+++ b/heat/engine/clients/os/keystone/__init__.py
@@ -14,11 +14,8 @@
from keystoneclient import exceptions
from heat.common import exception
-from heat.common import heat_keystoneclient as hkc
from heat.engine.clients import client_plugin
-from heat.engine import constraints
-
-CLIENT_NAME = 'keystone'
+from heat.engine.clients.os.keystone import heat_keystoneclient as hkc
class KeystoneClientPlugin(client_plugin.ClientPlugin):
@@ -132,63 +129,3 @@ class KeystoneClientPlugin(client_plugin.ClientPlugin):
except exceptions.NotFound:
raise exception.EntityNotFound(entity='KeystoneRegion',
name=region)
-
-
-class KeystoneBaseConstraint(constraints.BaseCustomConstraint):
-
- resource_client_name = CLIENT_NAME
- entity = None
-
- def validate_with_client(self, client, resource_id):
- # when user specify empty value in template, do not get the
- # responding resource from backend, otherwise an error will happen
- if resource_id == '':
- raise exception.EntityNotFound(entity=self.entity,
- name=resource_id)
-
- super(KeystoneBaseConstraint, self).validate_with_client(client,
- resource_id)
-
-
-class KeystoneRoleConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_role_id'
- entity = 'KeystoneRole'
-
-
-class KeystoneDomainConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_domain_id'
- entity = 'KeystoneDomain'
-
-
-class KeystoneProjectConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_project_id'
- entity = 'KeystoneProject'
-
-
-class KeystoneGroupConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_group_id'
- entity = 'KeystoneGroup'
-
-
-class KeystoneServiceConstraint(KeystoneBaseConstraint):
-
- expected_exceptions = (exception.EntityNotFound,
- exception.KeystoneServiceNameConflict,)
- resource_getter_name = 'get_service_id'
- entity = 'KeystoneService'
-
-
-class KeystoneUserConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_user_id'
- entity = 'KeystoneUser'
-
-
-class KeystoneRegionConstraint(KeystoneBaseConstraint):
-
- resource_getter_name = 'get_region_id'
- entity = 'KeystoneRegion'
diff --git a/heat/engine/clients/os/keystone/heat_keystoneclient.py b/heat/engine/clients/os/keystone/heat_keystoneclient.py
new file mode 100644
index 000000000..311a7eb67
--- /dev/null
+++ b/heat/engine/clients/os/keystone/heat_keystoneclient.py
@@ -0,0 +1,571 @@
+#
+# 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.
+
+"""Keystone Client functionality for use by resources."""
+
+import collections
+import uuid
+import weakref
+
+from keystoneauth1.identity import v3 as kc_auth_v3
+from keystoneauth1 import session
+import keystoneclient.exceptions as kc_exception
+from keystoneclient.v3 import client as kc_v3
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_serialization import jsonutils
+from oslo_utils import importutils
+
+from heat.common import config
+from heat.common import context
+from heat.common import exception
+from heat.common.i18n import _
+from heat.common.i18n import _LE
+from heat.common.i18n import _LW
+
+LOG = logging.getLogger('heat.engine.clients.keystoneclient')
+
+AccessKey = collections.namedtuple('AccessKey', ['id', 'access', 'secret'])
+
+_default_keystone_backend = (
+ 'heat.engine.clients.os.keystone.heat_keystoneclient.KsClientWrapper')
+
+keystone_opts = [
+ cfg.StrOpt('keystone_backend',
+ default=_default_keystone_backend,
+ help="Fully qualified class name to use as a keystone backend.")
+]
+cfg.CONF.register_opts(keystone_opts)
+
+
+class KsClientWrapper(object):
+ """Wrap keystone client so we can encapsulate logic used in resources.
+
+ Note this is intended to be initialized from a resource on a per-session
+ basis, so the session context is passed in on initialization
+ Also note that an instance of this is created in each request context as
+ part of a lazy-loaded cloud backend and it can be easily referenced in
+ each resource as ``self.keystone()``, so there should not be any need to
+ directly instantiate instances of this class inside resources themselves.
+ """
+
+ def __init__(self, context):
+ # If a trust_id is specified in the context, we immediately
+ # authenticate so we can populate the context with a trust token
+ # otherwise, we delay client authentication until needed to avoid
+ # unnecessary calls to keystone.
+ #
+ # Note that when you obtain a token using a trust, it cannot be
+ # used to reauthenticate and get another token, so we have to
+ # get a new trust-token even if context.auth_token is set.
+ #
+ # - context.auth_url is expected to contain a versioned keystone
+ # path, we will work with either a v2.0 or v3 path
+ self._context = weakref.ref(context)
+ self._client = None
+ self._admin_auth = None
+ self._domain_admin_auth = None
+ self._domain_admin_client = None
+
+ self.session = session.Session(**config.get_ssl_options('keystone'))
+ self.v3_endpoint = self.context.keystone_v3_endpoint
+
+ if self.context.trust_id:
+ # Create a client with the specified trust_id, this
+ # populates self.context.auth_token with a trust-scoped token
+ self._client = self._v3_client_init()
+
+ # The stack domain user ID should be set in heat.conf
+ # It can be created via python-openstackclient
+ # openstack --os-identity-api-version=3 domain create heat
+ # If the domain is specified, then you must specify a domain
+ # admin user. If no domain is specified, we fall back to
+ # legacy behavior with warnings.
+ self._stack_domain_id = cfg.CONF.stack_user_domain_id
+ self.stack_domain_name = cfg.CONF.stack_user_domain_name
+ self.domain_admin_user = cfg.CONF.stack_domain_admin
+ self.domain_admin_password = cfg.CONF.stack_domain_admin_password
+
+ LOG.debug('Using stack domain %s' % self.stack_domain)
+
+ @property
+ def context(self):
+ ctxt = self._context()
+ assert ctxt is not None, "Need a reference to the context"
+ return ctxt
+
+ @property
+ def stack_domain(self):
+ """Domain scope data.
+
+ This is only used for checking for scoping data, not using the value.
+ """
+ return self._stack_domain_id or self.stack_domain_name
+
+ @property
+ def client(self):
+ if not self._client:
+ # Create connection to v3 API
+ self._client = self._v3_client_init()
+ return self._client
+
+ @property
+ def domain_admin_auth(self):
+ if not self._domain_admin_auth:
+ # Note we must specify the domain when getting the token
+ # as only a domain scoped token can create projects in the domain
+ auth = kc_auth_v3.Password(username=self.domain_admin_user,
+ password=self.domain_admin_password,
+ auth_url=self.v3_endpoint,
+ domain_id=self._stack_domain_id,
+ domain_name=self.stack_domain_name,
+ user_domain_id=self._stack_domain_id,
+ user_domain_name=self.stack_domain_name)
+
+ # NOTE(jamielennox): just do something to ensure a valid token
+ try:
+ auth.get_token(self.session)
+ except kc_exception.Unauthorized:
+ LOG.error(_LE("Domain admin client authentication failed"))
+ raise exception.AuthorizationFailure()
+
+ self._domain_admin_auth = auth
+
+ return self._domain_admin_auth
+
+ @property
+ def domain_admin_client(self):
+ if not self._domain_admin_client:
+ self._domain_admin_client = kc_v3.Client(
+ session=self.session,
+ auth=self.domain_admin_auth)
+
+ return self._domain_admin_client
+
+ def _v3_client_init(self):
+ client = kc_v3.Client(session=self.session,
+ auth=self.context.auth_plugin)
+
+ if hasattr(self.context.auth_plugin, 'get_access'):
+ # NOTE(jamielennox): get_access returns the current token without
+ # reauthenticating if it's present and valid.
+ try:
+ auth_ref = self.context.auth_plugin.get_access(self.session)
+ except kc_exception.Unauthorized:
+ LOG.error(_LE("Keystone client authentication failed"))
+ raise exception.AuthorizationFailure()
+
+ if self.context.trust_id:
+ # Sanity check
+ if not auth_ref.trust_scoped:
+ LOG.error(_LE("trust token re-scoping failed!"))
+ raise exception.AuthorizationFailure()
+ # Sanity check that impersonation is effective
+ if self.context.trustor_user_id != auth_ref.user_id:
+ LOG.error(_LE("Trust impersonation failed"))
+ raise exception.AuthorizationFailure()
+
+ return client
+
+ def create_trust_context(self):
+ """Create a trust using the trustor identity in the current context.
+
+ The trust is created with the trustee as the heat service user.
+
+ If the current context already contains a trust_id, we do nothing
+ and return the current context.
+
+ Returns a context containing the new trust_id.
+ """
+ if self.context.trust_id:
+ return self.context
+
+ # We need the service admin user ID (not name), as the trustor user
+ # can't lookup the ID in keystoneclient unless they're admin
+ # workaround this by getting the user_id from admin_client
+
+ try:
+ trustee_user_id = self.context.trusts_auth_plugin.get_user_id(
+ self.session)
+ except kc_exception.Unauthorized:
+ LOG.error(_LE("Domain admin client authentication failed"))
+ raise exception.AuthorizationFailure()
+
+ trustor_user_id = self.context.auth_plugin.get_user_id(self.session)
+ trustor_proj_id = self.context.auth_plugin.get_project_id(self.session)
+
+ # inherit the roles of the trustor, unless set trusts_delegated_roles
+ if cfg.CONF.trusts_delegated_roles:
+ roles = cfg.CONF.trusts_delegated_roles
+ else:
+ roles = self.context.roles
+ try:
+ trust = self.client.trusts.create(trustor_user=trustor_user_id,
+ trustee_user=trustee_user_id,
+ project=trustor_proj_id,
+ impersonation=True,
+ role_names=roles)
+ except kc_exception.NotFound:
+ LOG.debug("Failed to find roles %s for user %s"
+ % (roles, trustor_user_id))
+ raise exception.MissingCredentialError(
+ required=_("roles %s") % roles)
+
+ context_data = self.context.to_dict()
+ context_data['overwrite'] = False
+ trust_context = context.RequestContext.from_dict(context_data)
+ trust_context.trust_id = trust.id
+ trust_context.trustor_user_id = trustor_user_id
+ return trust_context
+
+ def delete_trust(self, trust_id):
+ """Delete the specified trust."""
+ try:
+ self.client.trusts.delete(trust_id)
+ except kc_exception.NotFound:
+ pass
+
+ def _get_username(self, username):
+ if(len(username) > 64):
+ LOG.warning(_LW("Truncating the username %s to the last 64 "
+ "characters."), username)
+ # get the last 64 characters of the username
+ return username[-64:]
+
+ def create_stack_user(self, username, password=''):
+ """Create a user defined as part of a stack.
+
+ The user is defined either via template or created internally by a
+ resource. This user will be added to the heat_stack_user_role as
+ defined in the config.
+
+ Returns the keystone ID of the resulting user.
+ """
+ # FIXME(shardy): There's duplicated logic between here and
+ # create_stack_domain user, but this function is expected to
+ # be removed after the transition of all resources to domain
+ # users has been completed
+ stack_user_role = self.client.roles.list(
+ name=cfg.CONF.heat_stack_user_role)
+ if len(stack_user_role) == 1:
+ role_id = stack_user_role[0].id
+ # Create the user
+ user = self.client.users.create(
+ name=self._get_username(username), password=password,
+ default_project=self.context.tenant_id)
+ # Add user to heat_stack_user_role
+ LOG.debug("Adding user %(user)s to role %(role)s" % {
+ 'user': user.id, 'role': role_id})
+ self.client.roles.grant(role=role_id, user=user.id,
+ project=self.context.tenant_id)
+ else:
+ LOG.error(_LE("Failed to add user %(user)s to role %(role)s, "
+ "check role exists!"), {
+ 'user': username,
+ 'role': cfg.CONF.heat_stack_user_role})
+ raise exception.Error(_("Can't find role %s")
+ % cfg.CONF.heat_stack_user_role)
+
+ return user.id
+
+ def stack_domain_user_token(self, user_id, project_id, password):
+ """Get a token for a stack domain user."""
+ if not self.stack_domain:
+ # Note, no legacy fallback path as we don't want to deploy
+ # tokens for non stack-domain users inside instances
+ msg = _('Cannot get stack domain user token, no stack domain id '
+ 'configured, please fix your heat.conf')
+ raise exception.Error(msg)
+
+ # Create a keystoneclient session, then request a token with no
+ # catalog (the token is expected to be used inside an instance
+ # where a specific endpoint will be specified, and user-data
+ # space is limited..)
+ auth = kc_auth_v3.Password(auth_url=self.v3_endpoint,
+ user_id=user_id,
+ password=password,
+ project_id=project_id,
+ include_catalog=False)
+
+ return auth.get_token(self.session)
+
+ def create_stack_domain_user(self, username, project_id, password=None):
+ """Create a domain user defined as part of a stack.
+
+ The user is defined either via template or created internally by a
+ resource. This user will be added to the heat_stack_user_role as
+ defined in the config, and created in the specified project (which is
+ expected to be in the stack_domain).
+
+ Returns the keystone ID of the resulting user.
+ """
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.create_stack_user(username=username, password=password)
+ # We add the new user to a special keystone role
+ # This role is designed to allow easier differentiation of the
+ # heat-generated "stack users" which will generally have credentials
+ # deployed on an instance (hence are implicitly untrusted)
+ stack_user_role = self.domain_admin_client.roles.list(
+ name=cfg.CONF.heat_stack_user_role)
+ if len(stack_user_role) == 1:
+ role_id = stack_user_role[0].id
+ # Create user
+ user = self.domain_admin_client.users.create(
+ name=self._get_username(username), password=password,
+ default_project=project_id, domain=self.stack_domain_id)
+ # Add to stack user role
+ LOG.debug("Adding user %(user)s to role %(role)s" % {
+ 'user': user.id, 'role': role_id})
+ self.domain_admin_client.roles.grant(role=role_id, user=user.id,
+ project=project_id)
+ else:
+ LOG.error(_LE("Failed to add user %(user)s to role %(role)s, "
+ "check role exists!"),
+ {'user': username,
+ 'role': cfg.CONF.heat_stack_user_role})
+ raise exception.Error(_("Can't find role %s")
+ % cfg.CONF.heat_stack_user_role)
+
+ return user.id
+
+ @property
+ def stack_domain_id(self):
+ if not self._stack_domain_id:
+ try:
+ access = self.domain_admin_auth.get_access(self.session)
+ except kc_exception.Unauthorized:
+ LOG.error(_LE("Keystone client authentication failed"))
+ raise exception.AuthorizationFailure()
+
+ self._stack_domain_id = access.domain_id
+
+ return self._stack_domain_id
+
+ def _check_stack_domain_user(self, user_id, project_id, action):
+ """Sanity check that domain/project is correct."""
+ user = self.domain_admin_client.users.get(user_id)
+
+ if user.domain_id != self.stack_domain_id:
+ raise ValueError(_('User %s in invalid domain') % action)
+ if user.default_project_id != project_id:
+ raise ValueError(_('User %s in invalid project') % action)
+
+ def delete_stack_domain_user(self, user_id, project_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.delete_stack_user(user_id)
+
+ try:
+ self._check_stack_domain_user(user_id, project_id, 'delete')
+ self.domain_admin_client.users.delete(user_id)
+ except kc_exception.NotFound:
+ pass
+
+ def delete_stack_user(self, user_id):
+ try:
+ self.client.users.delete(user=user_id)
+ except kc_exception.NotFound:
+ pass
+
+ def create_stack_domain_project(self, stack_id):
+ """Create a project in the heat stack-user domain."""
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.context.tenant_id
+ # Note we use the tenant ID not name to ensure uniqueness in a multi-
+ # domain environment (where the tenant name may not be globally unique)
+ project_name = ('%s-%s' % (self.context.tenant_id, stack_id))[:64]
+ desc = "Heat stack user project"
+ domain_project = self.domain_admin_client.projects.create(
+ name=project_name,
+ domain=self.stack_domain_id,
+ description=desc)
+ return domain_project.id
+
+ def delete_stack_domain_project(self, project_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return
+
+ # If stacks are created before configuring the heat domain, they
+ # exist in the default domain, in the user's project, which we
+ # do *not* want to delete! However, if the keystone v3cloudsample
+ # policy is used, it's possible that we'll get Forbidden when trying
+ # to get the project, so again we should do nothing
+ try:
+ project = self.domain_admin_client.projects.get(project=project_id)
+ except kc_exception.NotFound:
+ return
+ except kc_exception.Forbidden:
+ LOG.warning(_LW('Unable to get details for project %s, '
+ 'not deleting'), project_id)
+ return
+
+ if project.domain_id != self.stack_domain_id:
+ LOG.warning(_LW('Not deleting non heat-domain project'))
+ return
+
+ try:
+ project.delete()
+ except kc_exception.NotFound:
+ pass
+
+ def _find_ec2_keypair(self, access, user_id=None):
+ """Lookup an ec2 keypair by access ID."""
+ # FIXME(shardy): add filtering for user_id when keystoneclient
+ # extensible-crud-manager-operations bp lands
+ credentials = self.client.credentials.list()
+ for cr in credentials:
+ ec2_creds = jsonutils.loads(cr.blob)
+ if ec2_creds.get('access') == access:
+ return AccessKey(id=cr.id,
+ access=ec2_creds['access'],
+ secret=ec2_creds['secret'])
+
+ def delete_ec2_keypair(self, credential_id=None, access=None,
+ user_id=None):
+ """Delete credential containing ec2 keypair."""
+ if credential_id:
+ try:
+ self.client.credentials.delete(credential_id)
+ except kc_exception.NotFound:
+ pass
+ elif access:
+ cred = self._find_ec2_keypair(access=access, user_id=user_id)
+ if cred:
+ self.client.credentials.delete(cred.id)
+ else:
+ raise ValueError("Must specify either credential_id or access")
+
+ def get_ec2_keypair(self, credential_id=None, access=None, user_id=None):
+ """Get an ec2 keypair via v3/credentials, by id or access."""
+ # Note v3/credentials does not support filtering by access
+ # because it's stored in the credential blob, so we expect
+ # all resources to pass credential_id except where backwards
+ # compatibility is required (resource only has access stored)
+ # then we'll have to do a brute-force lookup locally
+ if credential_id:
+ cred = self.client.credentials.get(credential_id)
+ ec2_creds = jsonutils.loads(cred.blob)
+ return AccessKey(id=cred.id,
+ access=ec2_creds['access'],
+ secret=ec2_creds['secret'])
+ elif access:
+ return self._find_ec2_keypair(access=access, user_id=user_id)
+ else:
+ raise ValueError("Must specify either credential_id or access")
+
+ def create_ec2_keypair(self, user_id=None):
+ user_id = user_id or self.context.get_access(self.session).user_id
+ project_id = self.context.tenant_id
+ data_blob = {'access': uuid.uuid4().hex,
+ 'secret': uuid.uuid4().hex}
+ ec2_creds = self.client.credentials.create(
+ user=user_id, type='ec2', data=jsonutils.dumps(data_blob),
+ project=project_id)
+
+ # Return a AccessKey namedtuple for easier access to the blob contents
+ # We return the id as the v3 api provides no way to filter by
+ # access in the blob contents, so it will be much more efficient
+ # if we manage credentials by ID instead
+ return AccessKey(id=ec2_creds.id,
+ access=data_blob['access'],
+ secret=data_blob['secret'])
+
+ def create_stack_domain_user_keypair(self, user_id, project_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.create_ec2_keypair(user_id)
+ data_blob = {'access': uuid.uuid4().hex,
+ 'secret': uuid.uuid4().hex}
+ creds = self.domain_admin_client.credentials.create(
+ user=user_id, type='ec2', data=jsonutils.dumps(data_blob),
+ project=project_id)
+ return AccessKey(id=creds.id,
+ access=data_blob['access'],
+ secret=data_blob['secret'])
+
+ def delete_stack_domain_user_keypair(self, user_id, project_id,
+ credential_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.delete_ec2_keypair(credential_id=credential_id)
+ self._check_stack_domain_user(user_id, project_id, 'delete_keypair')
+ try:
+ self.domain_admin_client.credentials.delete(credential_id)
+ except kc_exception.NotFound:
+ pass
+
+ def disable_stack_user(self, user_id):
+ self.client.users.update(user=user_id, enabled=False)
+
+ def enable_stack_user(self, user_id):
+ self.client.users.update(user=user_id, enabled=True)
+
+ def disable_stack_domain_user(self, user_id, project_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.disable_stack_user(user_id)
+ self._check_stack_domain_user(user_id, project_id, 'disable')
+ self.domain_admin_client.users.update(user=user_id, enabled=False)
+
+ def enable_stack_domain_user(self, user_id, project_id):
+ if not self.stack_domain:
+ # FIXME(shardy): Legacy fallback for folks using old heat.conf
+ # files which lack domain configuration
+ return self.enable_stack_user(user_id)
+ self._check_stack_domain_user(user_id, project_id, 'enable')
+ self.domain_admin_client.users.update(user=user_id, enabled=True)
+
+ def url_for(self, **kwargs):
+ default_region_name = (self.context.region_name or
+ cfg.CONF.region_name_for_services)
+ kwargs.setdefault('region_name', default_region_name)
+ return self.context.auth_plugin.get_endpoint(self.session, **kwargs)
+
+ @property
+ def auth_token(self):
+ return self.context.auth_plugin.get_token(self.session)
+
+ @property
+ def auth_ref(self):
+ return self.context.auth_plugin.get_access(self.session)
+
+
+class KeystoneClient(object):
+ """Keystone Auth Client.
+
+ Delay choosing the backend client module until the client's class
+ needs to be initialized.
+ """
+
+ def __new__(cls, context):
+ if cfg.CONF.keystone_backend == _default_keystone_backend:
+ return KsClientWrapper(context)
+ else:
+ return importutils.import_object(
+ cfg.CONF.keystone_backend,
+ context
+ )
+
+
+def list_opts():
+ yield None, keystone_opts
diff --git a/heat/engine/clients/os/keystone/keystone_constraints.py b/heat/engine/clients/os/keystone/keystone_constraints.py
new file mode 100644
index 000000000..ca0271a94
--- /dev/null
+++ b/heat/engine/clients/os/keystone/keystone_constraints.py
@@ -0,0 +1,77 @@
+#
+# 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 heat.common import exception
+from heat.engine import constraints
+
+CLIENT_NAME = 'keystone'
+
+
+class KeystoneBaseConstraint(constraints.BaseCustomConstraint):
+
+ resource_client_name = CLIENT_NAME
+ entity = None
+
+ def validate_with_client(self, client, resource_id):
+ # when user specify empty value in template, do not get the
+ # responding resource from backend, otherwise an error will happen
+ if resource_id == '':
+ raise exception.EntityNotFound(entity=self.entity,
+ name=resource_id)
+
+ super(KeystoneBaseConstraint, self).validate_with_client(client,
+ resource_id)
+
+
+class KeystoneRoleConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_role_id'
+ entity = 'KeystoneRole'
+
+
+class KeystoneDomainConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_domain_id'
+ entity = 'KeystoneDomain'
+
+
+class KeystoneProjectConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_project_id'
+ entity = 'KeystoneProject'
+
+
+class KeystoneGroupConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_group_id'
+ entity = 'KeystoneGroup'
+
+
+class KeystoneServiceConstraint(KeystoneBaseConstraint):
+
+ expected_exceptions = (exception.EntityNotFound,
+ exception.KeystoneServiceNameConflict,)
+ resource_getter_name = 'get_service_id'
+ entity = 'KeystoneService'
+
+
+class KeystoneUserConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_user_id'
+ entity = 'KeystoneUser'
+
+
+class KeystoneRegionConstraint(KeystoneBaseConstraint):
+
+ resource_getter_name = 'get_region_id'
+ entity = 'KeystoneRegion'