/
heat_keystoneclient.py
330 lines (291 loc) · 13.9 KB
/
heat_keystoneclient.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# 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 heat.common import context
from heat.common 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 heat.openstack.common import importutils
from heat.openstack.common import log as logging
logger = logging.getLogger('heat.common.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()
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_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()
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_v3.Client(**kwargs)
# Have to explicitly authenticate() or client.auth_ref is None
client.authenticate()
return client
def _get_client_option(self, option):
try:
cfg.CONF.import_opt(option, 'heat.common.config',
group='clients_keystone')
return getattr(cfg.CONF.clients_keystone, option)
except (cfg.NoSuchGroupError, cfg.NoSuchOptError):
cfg.CONF.import_opt(option, 'heat.common.config', group='clients')
return getattr(cfg.CONF.clients, option)
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@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_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