# # 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. """Client Library for Keystone Resources.""" from keystoneclient.v2_0 import client as kc from oslo_config import cfg from oslo_log import log as logging from oslo_utils import importutils from heat.common import exception from heat.common.i18n import _LE from heat.common.i18n import _LI from heat.common.i18n import _LW LOG = logging.getLogger('heat.common.keystoneclient') LOG.info(_LI("Keystone V2 loaded")) class KeystoneClientV2(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 a copy of this is created every resource as self.keystone() via the code in engine/client.py, 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 the v2.0 keystone endpoint self.context = context self._client = None if self.context.trust_id: # Create a connection to the v2 API, with the trust_id, this # populates self.context.auth_token with a trust-scoped token self._client = self._v2_client_init() @property def client(self): if not self._client: self._client = self._v2_client_init() return self._client def _v2_client_init(self): kwargs = { 'auth_url': self.context.auth_url, 'endpoint': self.context.auth_url, 'region_name': cfg.CONF.region_name_for_services } if self.context.region_name is not None: kwargs['region_name'] = self.context.region_name auth_kwargs = {} # Note try trust_id first, as we can't reuse auth_token in that case if self.context.trust_id is not None: # We got a trust_id, so we use the admin credentials # to authenticate, then re-scope the token to the # trust impersonating the trustor user. # Note that this currently requires the trustor tenant_id # to be passed to the authenticate(), unlike the v3 call kwargs.update(self._service_admin_creds()) auth_kwargs['trust_id'] = self.context.trust_id auth_kwargs['tenant_id'] = self.context.tenant_id elif self.context.auth_token is not None: kwargs['tenant_name'] = self.context.tenant kwargs['token'] = self.context.auth_token elif self.context.password is not None: kwargs['username'] = self.context.username kwargs['password'] = self.context.password kwargs['tenant_name'] = self.context.tenant kwargs['tenant_id'] = self.context.tenant_id else: LOG.error(_LE("Keystone v2 API connection failed, no password " "or auth_token!")) raise exception.AuthorizationFailure() kwargs['cacert'] = self._get_client_option('ca_file') kwargs['insecure'] = self._get_client_option('insecure') kwargs['cert'] = self._get_client_option('cert_file') kwargs['key'] = self._get_client_option('key_file') client = kc.Client(**kwargs) client.authenticate(**auth_kwargs) # If we are authenticating with a trust auth_kwargs are set, so set # the context auth_token with the re-scoped trust token if auth_kwargs: # Sanity check if not client.auth_ref.trust_scoped: LOG.error(_LE("v2 trust token re-scoping failed!")) raise exception.AuthorizationFailure() # All OK so update the context with the token self.context.auth_token = client.auth_ref.auth_token self.context.auth_url = kwargs.get('auth_url') # Ensure the v2 API we're using is not impacted by keystone # bug #1239303, otherwise we can't trust the user_id if self.context.trustor_user_id != client.auth_ref.user_id: LOG.error(_LE("Trust impersonation failed, bug #1239303 " "suspected, you may need a newer keystone")) raise exception.AuthorizationFailure() return client @staticmethod def _service_admin_creds(): # Import auth_token to have keystone_authtoken settings setup. importutils.import_module('keystonemiddleware.auth_token') creds = { 'username': cfg.CONF.keystone_authtoken.admin_user, 'password': cfg.CONF.keystone_authtoken.admin_password, 'auth_url': cfg.CONF.keystone_authtoken.auth_uri, 'tenant_name': cfg.CONF.keystone_authtoken.admin_tenant_name, } return creds def _get_client_option(self, option): # look for the option in the [clients_keystone] section # unknown options raise cfg.NoSuchOptError cfg.CONF.import_opt(option, 'heat.common.config', group='clients_keystone') v = getattr(cfg.CONF.clients_keystone, option) if v is not None: return v # look for the option in the generic [clients] section cfg.CONF.import_opt(option, 'heat.common.config', group='clients') return getattr(cfg.CONF.clients, option) def create_stack_user(self, username, password=''): """Create a user. User can be defined as part of a stack, 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 """ if len(username) > 64: LOG.warn(_LW("Truncating the username %s to the last 64 " "characters."), username) # get the last 64 characters of the username username = username[-64:] user = self.client.users.create(username, password, '%s@openstack.org' % username, tenant_id=self.context.tenant_id, enabled=True) # 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) roles = self.client.roles.list() stack_user_role = [r.id for r in roles if r.name == cfg.CONF.heat_stack_user_role] if len(stack_user_role) == 1: role_id = stack_user_role[0] LOG.debug("Adding user %(user)s to role %(role)s" % {'user': user.id, 'role': role_id}) self.client.roles.add_user_role(user.id, role_id, 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}) return user.id def delete_stack_user(self, user_id): self.client.users.delete(user_id) def delete_ec2_keypair(self, user_id, accesskey): self.client.ec2.delete(user_id, accesskey) def get_ec2_keypair(self, access, user_id=None): uid = user_id or self.client.auth_ref.user_id return self.client.ec2.get(uid, access) def create_ec2_keypair(self, user_id=None): uid = user_id or self.client.auth_ref.user_id return self.client.ec2.create(uid, self.context.tenant_id) def disable_stack_user(self, user_id): self.client.users.update_enabled(user_id, False) def enable_stack_user(self, user_id): self.client.users.update_enabled(user_id, True) def url_for(self, **kwargs): return self.client.service_catalog.url_for(**kwargs) @property def auth_token(self): return self.client.auth_token # ##################### # # V3 Compatible Methods # # ##################### # def create_stack_domain_user(self, username, project_id, password=None): return self.create_stack_user(username, password) def delete_stack_domain_user(self, user_id, project_id): return self.delete_stack_user(user_id) def create_stack_domain_project(self, project_id): """Use the tenant ID as domain project.""" return self.context.tenant_id def delete_stack_domain_project(self, project_id): """Pass through method since no project was created.""" pass def create_stack_domain_user_keypair(self, user_id, project_id): return self.create_ec2_keypair(user_id) def delete_stack_domain_user_keypair(self, user_id, project_id, credential_id): return self.delete_ec2_keypair(user_id, credential_id) # ###################### # # V3 Unsupported Methods # # ###################### # def create_trust_context(self): raise exception.NotSupported(feature='Keystone Trusts') def delete_trust(self, trust_id): raise exception.NotSupported(feature='Keystone Trusts')