1
# Copyright 2010 Jacob Kaplan-Moss
2
# Copyright 2011 OpenStack LLC.
3
# Copyright 2011 Piston Cloud Computing, Inc.
4
# Copyright 2011 Nebula, Inc.
8
OpenStack Client interface. Handles the REST calls and responses.
21
import simplejson as json
23
# Python 2.5 compat fix
24
if not hasattr(urlparse, 'parse_qsl'):
26
urlparse.parse_qsl = cgi.parse_qsl
29
from keystoneclient import access
30
from keystoneclient import exceptions
33
_logger = logging.getLogger(__name__)
37
keyring_available = True
42
if (hasattr(sys.stderr, 'isatty') and sys.stderr.isatty()):
43
print >> sys.stderr, 'Failed to load keyring modules.'
45
_logger.warning('Failed to load keyring modules.')
46
keyring_available = False
49
class HTTPClient(object):
51
USER_AGENT = 'python-keystoneclient'
57
def __init__(self, username=None, tenant_id=None, tenant_name=None,
58
password=None, auth_url=None, region_name=None, timeout=None,
59
endpoint=None, token=None, cacert=None, key=None,
60
cert=None, insecure=False, original_ip=None, debug=False,
61
auth_ref=None, use_keyring=False, force_new_token=False,
64
# set baseline defaults
67
self.tenant_name = None
70
self.auth_token = None
71
self.management_url = None
72
# if loading from a dictionary passed in via auth_ref,
73
# load values from AccessInfo parsing that dictionary
74
self.auth_ref = access.AccessInfo(**auth_ref) if auth_ref else None
76
self.username = self.auth_ref.username
77
self.tenant_id = self.auth_ref.tenant_id
78
self.tenant_name = self.auth_ref.tenant_name
79
self.auth_url = self.auth_ref.auth_url[0]
80
self.management_url = self.auth_ref.management_url[0]
81
self.auth_token = self.auth_ref.auth_token
82
# allow override of the auth_ref defaults from explicit
83
# values provided to the client
85
self.username = username
87
self.tenant_id = tenant_id
89
self.tenant_name = tenant_name
91
self.auth_url = auth_url.rstrip('/')
93
self.auth_token = token
95
self.management_url = endpoint.rstrip('/')
96
self.password = password
97
self.original_ip = original_ip
98
self.region_name = region_name
100
self.verify_cert = cacert
102
self.verify_cert = True
104
self.verify_cert = False
107
self.cert = (cert, key,)
111
self.debug_log = debug
113
ch = logging.StreamHandler()
114
_logger.setLevel(logging.DEBUG)
115
_logger.addHandler(ch)
116
self.requests_config['verbose'] = sys.stderr
119
self.use_keyring = use_keyring and keyring_available
120
self.force_new_token = force_new_token
121
self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION
122
self.stale_duration = int(self.stale_duration)
124
def authenticate(self, username=None, password=None, tenant_name=None,
125
tenant_id=None, auth_url=None, token=None):
126
""" Authenticate user.
128
Uses the data provided at instantiation to authenticate against
129
the Keystone server. This may use either a username and password
130
or token for authentication. If a tenant name or id was provided
131
then the resulting authenticated client will be scoped to that
132
tenant and contain a service catalog of available endpoints.
134
With the v2.0 API, if a tenant name or ID is not provided, the
135
authenication token returned will be 'unscoped' and limited in
136
capabilities until a fully-scoped token is acquired.
138
If successful, sets the self.auth_ref and self.auth_token with
139
the returned token. If not already set, will also set
140
self.management_url from the details provided in the token.
142
:returns: ``True`` if authentication was successful.
143
:raises: AuthorizationFailure if unable to authenticate or validate
144
the existing authorization token
145
:raises: ValueError if insufficient parameters are used.
147
If keyring is used, token is retrieved from keyring instead.
148
Authentication will only be necessary if any of the following
151
* keyring is not used
152
* if token is not found in keyring
153
* if token retrieved from keyring is expired or about to
154
expired (as determined by stale_duration)
155
* if force_new_token is true
158
auth_url = auth_url or self.auth_url
159
username = username or self.username
160
password = password or self.password
161
tenant_name = tenant_name or self.tenant_name
162
tenant_id = tenant_id or self.tenant_id
163
token = token or self.auth_token
165
(keyring_key, auth_ref) = self.get_auth_ref_from_keyring(auth_url,
170
new_token_needed = False
171
if auth_ref is None or self.force_new_token:
172
new_token_needed = True
173
raw_token = self.get_raw_token_from_identity_service(auth_url,
179
self.auth_ref = access.AccessInfo(**raw_token)
181
self.auth_ref = auth_ref
184
self.store_auth_ref_into_keyring(keyring_key)
187
def _build_keyring_key(self, auth_url, username, tenant_name,
189
""" Create a unique key for keyring.
191
Used to store and retrieve auth_ref from keyring.
194
keys = [auth_url, username, tenant_name, tenant_id, token]
195
for index, key in enumerate(keys):
198
keyring_key = '/'.join(keys)
201
def get_auth_ref_from_keyring(self, auth_url, username, tenant_name,
203
""" Retrieve auth_ref from keyring.
205
If auth_ref is found in keyring, (keyring_key, auth_ref) is returned.
206
Otherwise, (keyring_key, None) is returned.
208
:returns: (keyring_key, auth_ref) or (keyring_key, None)
214
keyring_key = self._build_keyring_key(auth_url, username,
215
tenant_name, tenant_id,
218
auth_ref = keyring.get_password("keystoneclient_auth",
221
auth_ref = pickle.loads(auth_ref)
222
if auth_ref.will_expire_soon(self.stale_duration):
223
# token has expired, don't use it
225
except Exception as e:
227
_logger.warning('Unable to retrieve token from keyring %s' % (
229
return (keyring_key, auth_ref)
231
def store_auth_ref_into_keyring(self, keyring_key):
232
""" Store auth_ref into keyring.
237
keyring.set_password("keystoneclient_auth",
239
pickle.dumps(self.auth_ref))
240
except Exception as e:
241
_logger.warning("Failed to store token into keyring %s" % (e))
243
def process_token(self):
244
""" Extract and process information from the new auth_ref.
247
raise NotImplementedError
249
def get_raw_token_from_identity_service(self, auth_url, username=None,
250
password=None, tenant_name=None,
251
tenant_id=None, token=None):
252
""" Authenticate against the Identity API and get a token.
254
Not implemented here because auth protocols should be API
257
Expected to authenticate or validate an existing authentication
258
reference already associated with the client. Invoking this call
259
*always* makes a call to the Keystone.
261
:returns: ``raw token``
264
raise NotImplementedError
266
def _extract_service_catalog(self, url, body):
267
""" Set the client's service catalog from the response data.
269
Not implemented here because data returned may be API
272
raise NotImplementedError
274
def http_log_req(self, args, kwargs):
275
if not self.debug_log:
278
string_parts = ['curl -i']
280
if element in ('GET', 'POST'):
281
string_parts.append(' -X %s' % element)
283
string_parts.append(' %s' % element)
285
for element in kwargs['headers']:
286
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
287
string_parts.append(header)
289
_logger.debug("REQ: %s" % "".join(string_parts))
291
_logger.debug("REQ BODY: %s\n" % (kwargs['body']))
293
def http_log_resp(self, resp):
296
"RESP: [%s] %s\nRESP BODY: %s\n",
301
def serialize(self, entity):
302
return json.dumps(entity)
304
def request(self, url, method, **kwargs):
305
""" Send an http request with the specified characteristics.
307
Wrapper around requests.request to handle tasks such as
308
setting headers, JSON encoding/decoding, and error handling.
310
# Copy the kwargs so we can reuse the original in case of redirects
311
request_kwargs = copy.copy(kwargs)
312
request_kwargs.setdefault('headers', kwargs.get('headers', {}))
313
request_kwargs['headers']['User-Agent'] = self.USER_AGENT
315
request_kwargs['headers']['Forwarded'] = "for=%s;by=%s" % (
316
self.original_ip, self.USER_AGENT)
318
request_kwargs['headers']['Content-Type'] = 'application/json'
319
request_kwargs['data'] = self.serialize(kwargs['body'])
320
del request_kwargs['body']
322
request_kwargs['cert'] = self.cert
324
self.http_log_req((url, method,), request_kwargs)
325
resp = requests.request(
328
verify=self.verify_cert,
329
config=self.requests_config,
332
self.http_log_resp(resp)
334
if resp.status_code in (400, 401, 403, 404, 408, 409, 413, 500, 501):
336
"Request returned failure status: %s",
338
raise exceptions.from_response(resp, resp.text)
339
elif resp.status_code in (301, 302, 305):
340
# Redirected. Reissue the request to the new location.
341
return self.request(resp.headers['location'], method, **kwargs)
345
body = json.loads(resp.text)
348
_logger.debug("Could not decode JSON from body: %s"
351
_logger.debug("No body was returned.")
356
def _cs_request(self, url, method, **kwargs):
357
""" Makes an authenticated request to keystone endpoint by
358
concatenating self.management_url and url and passing in method and
359
any associated kwargs. """
361
is_management = kwargs.pop('management', True)
363
if is_management and self.management_url is None:
364
raise exceptions.AuthorizationFailure(
365
'Current authorization does not have a known management url')
367
url_to_use = self.auth_url
369
url_to_use = self.management_url
371
kwargs.setdefault('headers', {})
373
kwargs['headers']['X-Auth-Token'] = self.auth_token
375
resp, body = self.request(url_to_use + url, method,
379
def get(self, url, **kwargs):
380
return self._cs_request(url, 'GET', **kwargs)
382
def head(self, url, **kwargs):
383
return self._cs_request(url, 'HEAD', **kwargs)
385
def post(self, url, **kwargs):
386
return self._cs_request(url, 'POST', **kwargs)
388
def put(self, url, **kwargs):
389
return self._cs_request(url, 'PUT', **kwargs)
391
def patch(self, url, **kwargs):
392
return self._cs_request(url, 'PATCH', **kwargs)
394
def delete(self, url, **kwargs):
395
return self._cs_request(url, 'DELETE', **kwargs)