# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # 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 oslo_context import context from nova import exception import eventlet from keystoneclient.v2_0 import client as kc from keystoneclient.v3 import client as kc_v3 from oslo.config import cfg from oslo.utils import importutils from oslo_log import log as logging logger = logging.getLogger('nova.compute.keystoneclient') class KeystoneClient(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): # We have to maintain two clients authenticated with keystone: # - ec2 interface is v2.0 only # - trusts is v3 only # 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_v2 = None self._client_v3 = 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_v2 = self._v2_client_init() @property def client_v3(self): if not self._client_v3: # Create connection to v3 API self._client_v3 = self._v3_client_init() return self._client_v3 @property def client_v2(self): if not self._client_v2: self._client_v2 = self._v2_client_init() return self._client_v2 def _v2_client_init(self): kwargs = { 'auth_url': self.context.auth_url } 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(api_version=2)) 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: logger.error("Keystone v2 API connection failed, no password or " "auth_token!") raise exception.AuthorizationFailure() client_v2 = kc.Client(**kwargs) client_v2.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_v2.auth_ref.trust_scoped: logger.error("v2 trust token re-scoping failed!") raise exception.AuthorizationFailure() # All OK so update the context with the token self.context.auth_token = client_v2.auth_ref.auth_token self.context.auth_url = kwargs.get('auth_url') return client_v2 @staticmethod def _service_admin_creds(api_version=2): # Import auth_token to have keystone_authtoken settings setup. importutils.import_module('keystoneclient.middleware.auth_token') creds = { 'username': cfg.CONF.keystone_authtoken.admin_user, 'password': cfg.CONF.keystone_authtoken.admin_password, } if api_version >= 3: creds['auth_url'] =\ cfg.CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3') creds['project_name'] =\ cfg.CONF.keystone_authtoken.admin_tenant_name else: creds['auth_url'] = cfg.CONF.keystone_authtoken.auth_uri creds['tenant_name'] =\ cfg.CONF.keystone_authtoken.admin_tenant_name return creds def _v3_client_init(self): kwargs = {} if self.context.auth_token is not None: kwargs['project_name'] = self.context.tenant kwargs['token'] = self.context.auth_token kwargs['auth_url'] = self.context.auth_url.replace('v2.0', 'v3') kwargs['endpoint'] = kwargs['auth_url'] elif self.context.trust_id is not None: # We got a trust_id, so we use the admin credentials and get a # Token back impersonating the trustor user kwargs.update(self._service_admin_creds(api_version=3)) kwargs['trust_id'] = self.context.trust_id elif self.context.password is not None: kwargs['username'] = self.context.username kwargs['password'] = self.context.password kwargs['project_name'] = self.context.tenant kwargs['project_id'] = self.context.tenant_id kwargs['auth_url'] = self.context.auth_url.replace('v2.0', 'v3') kwargs['endpoint'] = kwargs['auth_url'] else: logger.error("Keystone v3 API connection failed, no password or " "auth_token!") raise exception.AuthorizationFailure() client = kc_v3.Client(**kwargs) # Have to explicitly authenticate() or client.auth_ref is None client.authenticate() return client def create_trust_context(self): """ If cfg.CONF.deferred_auth_method is trusts, we create a trust using the trustor identity in the current context, with the trustee as the heat service user and return a context containing the new trust_id If deferred_auth_method != trusts, or the current context already contains a trust_id, we do nothing and return the current context """ 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 creating a temporary admin client connection # then getting the user ID from the auth_ref admin_creds = self._service_admin_creds() admin_client = kc.Client(**admin_creds) trustee_user_id = admin_client.auth_ref.user_id trustor_user_id = self.client_v3.auth_ref.user_id trustor_project_id = self.client_v3.auth_ref.project_id roles = cfg.CONF.trusts_delegated_roles trust = self.client_v3.trusts.create(trustor_user=trustor_user_id, trustee_user=trustee_user_id, project=trustor_project_id, impersonation=True, role_names=roles) trust_context = context.RequestContext.from_dict( self.context.to_dict()) 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. """ self.client_v3.trusts.delete(trust_id) def create_stack_user(self, username, password=''): """ Create a user 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): logger.warning("Truncating the username %s to the last 64 " "characters." % username) # get the last 64 characters of the username username = username[-64:] user = self.client_v2.users.create(username, password, '%s@heat-api.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_v2.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] logger.debug("Adding user %s to role %s" % (user.id, role_id)) self.client_v2.roles.add_user_role(user.id, role_id, self.context.tenant_id) else: logger.error("Failed to add user %s to role %s, check role exists!" % (username, cfg.CONF.heat_stack_user_role)) return user.id def delete_stack_user(self, user_id): user = self.client_v2.users.get(user_id) # FIXME (shardy) : need to test, do we still need this retry logic? # Copied from user.py, but seems like something we really shouldn't # need to do, no bug reference in the original comment (below)... # tempory hack to work around an openstack bug. # seems you can't delete a user first time - you have to try # a couple of times - go figure! tmo = eventlet.Timeout(10) status = 'WAITING' reason = 'Timed out trying to delete user' try: while status == 'WAITING': try: user.delete() status = 'DELETED' except Exception as ce: reason = str(ce) logger.warning("Problem deleting user %s: %s" % (user_id, reason)) eventlet.sleep(1) except eventlet.Timeout as t: if t is not tmo: # not my timeout raise else: status = 'TIMEDOUT' finally: tmo.cancel() if status != 'DELETED': raise exception.Error(reason) def delete_ec2_keypair(self, user_id, accesskey): self.client_v2.ec2.delete(user_id, accesskey) def get_ec2_keypair(self, user_id): # We make the assumption that each user will only have one # ec2 keypair, it's not clear if AWS allow multiple AccessKey resources # to be associated with a single User resource, but for simplicity # we assume that here for now cred = self.client_v2.ec2.list(user_id) if len(cred) == 0: return self.client_v2.ec2.create(user_id, self.context.tenant_id) if len(cred) == 1: return cred[0] else: logger.error("Unexpected number of ec2 credentials %s for %s" % (len(cred), user_id)) def disable_stack_user(self, user_id): # FIXME : This won't work with the v3 keystone API self.client_v2.users.update_enabled(user_id, False) def enable_stack_user(self, user_id): # FIXME : This won't work with the v3 keystone API self.client_v2.users.update_enabled(user_id, True) def url_for(self, **kwargs): return self.client_v2.service_catalog.url_for(**kwargs) @property def auth_token(self): return self.client_v2.auth_token