2
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3
# not use this file except in compliance with the License. You may obtain
4
# a copy of the License at
6
# http://www.apache.org/licenses/LICENSE-2.0
8
# Unless required by applicable law or agreed to in writing, software
9
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11
# License for the specific language governing permissions and limitations
14
"""Client Library for Keystone Resources."""
16
from keystoneclient.v2_0 import client as kc
17
from oslo_config import cfg
18
from oslo_log import log as logging
19
from oslo_utils import importutils
21
from heat.common import exception
22
from heat.common.i18n import _LE
23
from heat.common.i18n import _LI
24
from heat.common.i18n import _LW
26
LOG = logging.getLogger('heat.common.keystoneclient')
27
LOG.info(_LI("Keystone V2 loaded"))
30
class KeystoneClientV2(object):
32
"""Wrap keystone client so we can encapsulate logic used in resources.
34
Note: This is intended to be initialized from a resource on a per-session
35
basis, so the session context is passed in on initialization
36
Also note that a copy of this is created every resource as self.keystone()
37
via the code in engine/client.py, so there should not be any need to
38
directly instantiate instances of this class inside resources themselves.
40
def __init__(self, context):
41
# If a trust_id is specified in the context, we immediately
42
# authenticate so we can populate the context with a trust token
43
# otherwise, we delay client authentication until needed to avoid
44
# unnecessary calls to keystone.
46
# Note that when you obtain a token using a trust, it cannot be
47
# used to reauthenticate and get another token, so we have to
48
# get a new trust-token even if context.auth_token is set.
50
# - context.auth_url is expected to contain the v2.0 keystone endpoint
51
self.context = context
54
if self.context.trust_id:
55
# Create a connection to the v2 API, with the trust_id, this
56
# populates self.context.auth_token with a trust-scoped token
57
self._client = self._v2_client_init()
62
self._client = self._v2_client_init()
65
def _v2_client_init(self):
67
'auth_url': self.context.auth_url,
68
'endpoint': self.context.auth_url,
69
'region_name': cfg.CONF.region_name_for_services
72
if self.context.region_name is not None:
73
kwargs['region_name'] = self.context.region_name
76
# Note try trust_id first, as we can't reuse auth_token in that case
77
if self.context.trust_id is not None:
78
# We got a trust_id, so we use the admin credentials
79
# to authenticate, then re-scope the token to the
80
# trust impersonating the trustor user.
81
# Note that this currently requires the trustor tenant_id
82
# to be passed to the authenticate(), unlike the v3 call
83
kwargs.update(self._service_admin_creds())
84
auth_kwargs['trust_id'] = self.context.trust_id
85
auth_kwargs['tenant_id'] = self.context.tenant_id
86
elif self.context.auth_token is not None:
87
kwargs['tenant_name'] = self.context.tenant
88
kwargs['token'] = self.context.auth_token
89
elif self.context.password is not None:
90
kwargs['username'] = self.context.username
91
kwargs['password'] = self.context.password
92
kwargs['tenant_name'] = self.context.tenant
93
kwargs['tenant_id'] = self.context.tenant_id
95
LOG.error(_LE("Keystone v2 API connection failed, no password "
97
raise exception.AuthorizationFailure()
98
kwargs['cacert'] = self._get_client_option('ca_file')
99
kwargs['insecure'] = self._get_client_option('insecure')
100
kwargs['cert'] = self._get_client_option('cert_file')
101
kwargs['key'] = self._get_client_option('key_file')
102
client = kc.Client(**kwargs)
104
client.authenticate(**auth_kwargs)
105
# If we are authenticating with a trust auth_kwargs are set, so set
106
# the context auth_token with the re-scoped trust token
109
if not client.auth_ref.trust_scoped:
110
LOG.error(_LE("v2 trust token re-scoping failed!"))
111
raise exception.AuthorizationFailure()
112
# All OK so update the context with the token
113
self.context.auth_token = client.auth_ref.auth_token
114
self.context.auth_url = kwargs.get('auth_url')
115
# Ensure the v2 API we're using is not impacted by keystone
116
# bug #1239303, otherwise we can't trust the user_id
117
if self.context.trustor_user_id != client.auth_ref.user_id:
118
LOG.error(_LE("Trust impersonation failed, bug #1239303 "
119
"suspected, you may need a newer keystone"))
120
raise exception.AuthorizationFailure()
125
def _service_admin_creds():
126
# Import auth_token to have keystone_authtoken settings setup.
127
importutils.import_module('keystonemiddleware.auth_token')
130
'username': cfg.CONF.keystone_authtoken.admin_user,
131
'password': cfg.CONF.keystone_authtoken.admin_password,
132
'auth_url': cfg.CONF.keystone_authtoken.auth_uri,
133
'tenant_name': cfg.CONF.keystone_authtoken.admin_tenant_name,
138
def _get_client_option(self, option):
139
# look for the option in the [clients_keystone] section
140
# unknown options raise cfg.NoSuchOptError
141
cfg.CONF.import_opt(option, 'heat.common.config',
142
group='clients_keystone')
143
v = getattr(cfg.CONF.clients_keystone, option)
146
# look for the option in the generic [clients] section
147
cfg.CONF.import_opt(option, 'heat.common.config', group='clients')
148
return getattr(cfg.CONF.clients, option)
150
def create_stack_user(self, username, password=''):
152
Create a user defined as part of a stack, either via template
153
or created internally by a resource. This user will be added to
154
the heat_stack_user_role as defined in the config
155
Returns the keystone ID of the resulting user
157
if(len(username) > 64):
158
LOG.warn(_LW("Truncating the username %s to the last 64 "
159
"characters."), username)
160
# get the last 64 characters of the username
161
username = username[-64:]
162
user = self.client.users.create(username,
164
'%s@openstack.org' % username,
165
tenant_id=self.context.tenant_id,
168
# We add the new user to a special keystone role
169
# This role is designed to allow easier differentiation of the
170
# heat-generated "stack users" which will generally have credentials
171
# deployed on an instance (hence are implicitly untrusted)
172
roles = self.client.roles.list()
173
stack_user_role = [r.id for r in roles
174
if r.name == cfg.CONF.heat_stack_user_role]
175
if len(stack_user_role) == 1:
176
role_id = stack_user_role[0]
177
LOG.debug("Adding user %(user)s to role %(role)s"
178
% {'user': user.id, 'role': role_id})
179
self.client.roles.add_user_role(user.id, role_id,
180
self.context.tenant_id)
182
LOG.error(_LE("Failed to add user %(user)s to role %(role)s, "
183
"check role exists!"),
185
'role': cfg.CONF.heat_stack_user_role})
189
def delete_stack_user(self, user_id):
190
self.client.users.delete(user_id)
192
def delete_ec2_keypair(self, user_id, accesskey):
193
self.client.ec2.delete(user_id, accesskey)
195
def get_ec2_keypair(self, access, user_id=None):
196
uid = user_id or self.client.auth_ref.user_id
197
return self.client.ec2.get(uid, access)
199
def create_ec2_keypair(self, user_id=None):
200
uid = user_id or self.client.auth_ref.user_id
201
return self.client.ec2.create(uid, self.context.tenant_id)
203
def disable_stack_user(self, user_id):
204
self.client.users.update_enabled(user_id, False)
206
def enable_stack_user(self, user_id):
207
self.client.users.update_enabled(user_id, True)
209
def url_for(self, **kwargs):
210
return self.client.service_catalog.url_for(**kwargs)
213
def auth_token(self):
214
return self.client.auth_token
216
# ##################### #
217
# V3 Compatible Methods #
218
# ##################### #
220
def create_stack_domain_user(self, username, project_id, password=None):
221
return self.create_stack_user(username, password)
223
def delete_stack_domain_user(self, user_id, project_id):
224
return self.delete_stack_user(user_id)
226
def create_stack_domain_project(self, project_id):
227
'''Use the tenant ID as domain project.'''
228
return self.context.tenant_id
230
def delete_stack_domain_project(self, project_id):
231
'''Pass through method since no project was created.'''
234
def create_stack_domain_user_keypair(self, user_id, project_id):
235
return self.create_ec2_keypair(user_id)
237
def delete_stack_domain_user_keypair(self, user_id, project_id,
239
return self.delete_ec2_keypair(user_id, credential_id)
241
# ###################### #
242
# V3 Unsupported Methods #
243
# ###################### #
245
def create_trust_context(self):
246
raise exception.NotSupported(feature='Keystone Trusts')
248
def delete_trust(self, trust_id):
249
raise exception.NotSupported(feature='Keystone Trusts')