
When nova uses neutron-client and the client needs to authenticate with keystone it will additionally retrieve the service-catalog. The client will then ignore the provided endpoint_url parameter given to it during instantiation because the service-catalog retrieval process overrides that parameter. Using the endpoint declared in the service-catalog even though the endpoint is given is unexpected given the precedent set by glance-client to respect that parameter. Ensuring the parameter is used will allow users to override the identity's catalog. This is useful if identity is in error, or if the user is using a special development configuration. Unit tests are included to test that said functionality occurs. Fixes: bug #1212812 Change-Id: Iad2eaf4096fd5278740c3664cc369853814ee2d5
284 lines
11 KiB
Python
284 lines
11 KiB
Python
# Copyright 2012 OpenStack LLC.
|
|
# All Rights Reserved
|
|
#
|
|
# 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.
|
|
#
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
import logging
|
|
import os
|
|
import urlparse
|
|
# Python 2.5 compat fix
|
|
if not hasattr(urlparse, 'parse_qsl'):
|
|
import cgi
|
|
urlparse.parse_qsl = cgi.parse_qsl
|
|
|
|
import httplib2
|
|
|
|
from neutronclient.common import exceptions
|
|
from neutronclient.common import utils
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
if os.environ.get('NEUTRONCLIENT_DEBUG'):
|
|
ch = logging.StreamHandler()
|
|
_logger.setLevel(logging.DEBUG)
|
|
_logger.addHandler(ch)
|
|
|
|
|
|
class ServiceCatalog(object):
|
|
"""Helper methods for dealing with a Keystone Service Catalog."""
|
|
|
|
def __init__(self, resource_dict):
|
|
self.catalog = resource_dict
|
|
|
|
def get_token(self):
|
|
"""Fetch token details fron service catalog."""
|
|
token = {'id': self.catalog['access']['token']['id'],
|
|
'expires': self.catalog['access']['token']['expires'], }
|
|
try:
|
|
token['user_id'] = self.catalog['access']['user']['id']
|
|
token['tenant_id'] = (
|
|
self.catalog['access']['token']['tenant']['id'])
|
|
except Exception:
|
|
# just leave the tenant and user out if it doesn't exist
|
|
pass
|
|
return token
|
|
|
|
def url_for(self, attr=None, filter_value=None,
|
|
service_type='network', endpoint_type='publicURL'):
|
|
"""Fetch the URL from the Neutron service for
|
|
a particular endpoint type. If none given, return
|
|
publicURL.
|
|
"""
|
|
|
|
catalog = self.catalog['access'].get('serviceCatalog', [])
|
|
matching_endpoints = []
|
|
for service in catalog:
|
|
if service['type'] != service_type:
|
|
continue
|
|
|
|
endpoints = service['endpoints']
|
|
for endpoint in endpoints:
|
|
if not filter_value or endpoint.get(attr) == filter_value:
|
|
matching_endpoints.append(endpoint)
|
|
|
|
if not matching_endpoints:
|
|
raise exceptions.EndpointNotFound()
|
|
elif len(matching_endpoints) > 1:
|
|
raise exceptions.AmbiguousEndpoints(message=matching_endpoints)
|
|
else:
|
|
if endpoint_type not in matching_endpoints[0]:
|
|
raise exceptions.EndpointTypeNotFound(message=endpoint_type)
|
|
|
|
return matching_endpoints[0][endpoint_type]
|
|
|
|
|
|
class HTTPClient(httplib2.Http):
|
|
"""Handles the REST calls and responses, include authn."""
|
|
|
|
USER_AGENT = 'python-neutronclient'
|
|
|
|
def __init__(self, username=None, tenant_name=None, tenant_id=None,
|
|
password=None, auth_url=None,
|
|
token=None, region_name=None, timeout=None,
|
|
endpoint_url=None, insecure=False,
|
|
endpoint_type='publicURL',
|
|
auth_strategy='keystone', ca_cert=None, log_credentials=False,
|
|
**kwargs):
|
|
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert)
|
|
|
|
self.username = username
|
|
self.tenant_name = tenant_name
|
|
self.tenant_id = tenant_id
|
|
self.password = password
|
|
self.auth_url = auth_url.rstrip('/') if auth_url else None
|
|
self.endpoint_type = endpoint_type
|
|
self.region_name = region_name
|
|
self.auth_token = token
|
|
self.content_type = 'application/json'
|
|
self.endpoint_url = endpoint_url
|
|
self.auth_strategy = auth_strategy
|
|
self.log_credentials = log_credentials
|
|
# httplib2 overrides
|
|
self.disable_ssl_certificate_validation = insecure
|
|
|
|
def _cs_request(self, *args, **kwargs):
|
|
kargs = {}
|
|
kargs.setdefault('headers', kwargs.get('headers', {}))
|
|
kargs['headers']['User-Agent'] = self.USER_AGENT
|
|
|
|
if 'content_type' in kwargs:
|
|
kargs['headers']['Content-Type'] = kwargs['content_type']
|
|
kargs['headers']['Accept'] = kwargs['content_type']
|
|
else:
|
|
kargs['headers']['Content-Type'] = self.content_type
|
|
kargs['headers']['Accept'] = self.content_type
|
|
|
|
if 'body' in kwargs:
|
|
kargs['body'] = kwargs['body']
|
|
args = utils.safe_encode_list(args)
|
|
kargs = utils.safe_encode_dict(kargs)
|
|
|
|
if self.log_credentials:
|
|
log_kargs = kargs
|
|
else:
|
|
log_kargs = self._strip_credentials(kargs)
|
|
|
|
utils.http_log_req(_logger, args, log_kargs)
|
|
try:
|
|
resp, body = self.request(*args, **kargs)
|
|
except httplib2.SSLHandshakeError as e:
|
|
raise exceptions.SslCertificateValidationError(reason=e)
|
|
except Exception as e:
|
|
# Wrap the low-level connection error (socket timeout, redirect
|
|
# limit, decompression error, etc) into our custom high-level
|
|
# connection exception (it is excepted in the upper layers of code)
|
|
raise exceptions.ConnectionFailed(reason=e)
|
|
utils.http_log_resp(_logger, resp, body)
|
|
status_code = self.get_status_code(resp)
|
|
if status_code == 401:
|
|
raise exceptions.Unauthorized(message=body)
|
|
elif status_code == 403:
|
|
raise exceptions.Forbidden(message=body)
|
|
return resp, body
|
|
|
|
def _strip_credentials(self, kwargs):
|
|
if kwargs.get('body') and self.password:
|
|
log_kwargs = kwargs.copy()
|
|
log_kwargs['body'] = kwargs['body'].replace(self.password,
|
|
'REDACTED')
|
|
return log_kwargs
|
|
else:
|
|
return kwargs
|
|
|
|
def authenticate_and_fetch_endpoint_url(self):
|
|
if not self.auth_token:
|
|
self.authenticate()
|
|
elif not self.endpoint_url:
|
|
self.endpoint_url = self._get_endpoint_url()
|
|
|
|
def do_request(self, url, method, **kwargs):
|
|
self.authenticate_and_fetch_endpoint_url()
|
|
# Perform the request once. If we get a 401 back then it
|
|
# might be because the auth token expired, so try to
|
|
# re-authenticate and try again. If it still fails, bail.
|
|
try:
|
|
kwargs.setdefault('headers', {})
|
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
|
resp, body = self._cs_request(self.endpoint_url + url, method,
|
|
**kwargs)
|
|
return resp, body
|
|
except exceptions.Unauthorized:
|
|
self.authenticate()
|
|
kwargs.setdefault('headers', {})
|
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
|
resp, body = self._cs_request(
|
|
self.endpoint_url + url, method, **kwargs)
|
|
return resp, body
|
|
|
|
def _extract_service_catalog(self, body):
|
|
"""Set the client's service catalog from the response data."""
|
|
self.service_catalog = ServiceCatalog(body)
|
|
try:
|
|
sc = self.service_catalog.get_token()
|
|
self.auth_token = sc['id']
|
|
self.auth_tenant_id = sc.get('tenant_id')
|
|
self.auth_user_id = sc.get('user_id')
|
|
except KeyError:
|
|
raise exceptions.Unauthorized()
|
|
if not self.endpoint_url:
|
|
self.endpoint_url = self.service_catalog.url_for(
|
|
attr='region', filter_value=self.region_name,
|
|
endpoint_type=self.endpoint_type)
|
|
|
|
def authenticate(self):
|
|
if self.auth_strategy != 'keystone':
|
|
raise exceptions.Unauthorized(message='unknown auth strategy')
|
|
if self.tenant_id:
|
|
body = {'auth': {'passwordCredentials':
|
|
{'username': self.username,
|
|
'password': self.password, },
|
|
'tenantId': self.tenant_id, }, }
|
|
else:
|
|
body = {'auth': {'passwordCredentials':
|
|
{'username': self.username,
|
|
'password': self.password, },
|
|
'tenantName': self.tenant_name, }, }
|
|
|
|
token_url = self.auth_url + "/tokens"
|
|
|
|
# Make sure we follow redirects when trying to reach Keystone
|
|
tmp_follow_all_redirects = self.follow_all_redirects
|
|
self.follow_all_redirects = True
|
|
try:
|
|
resp, body = self._cs_request(token_url, "POST",
|
|
body=json.dumps(body),
|
|
content_type="application/json")
|
|
finally:
|
|
self.follow_all_redirects = tmp_follow_all_redirects
|
|
status_code = self.get_status_code(resp)
|
|
if status_code != 200:
|
|
raise exceptions.Unauthorized(message=body)
|
|
if body:
|
|
try:
|
|
body = json.loads(body)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
body = None
|
|
self._extract_service_catalog(body)
|
|
|
|
def _get_endpoint_url(self):
|
|
url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
|
|
try:
|
|
resp, body = self._cs_request(url, "GET")
|
|
except exceptions.Unauthorized:
|
|
# rollback to authenticate() to handle case when neutron client
|
|
# is initialized just before the token is expired
|
|
self.authenticate()
|
|
return self.endpoint_url
|
|
|
|
body = json.loads(body)
|
|
for endpoint in body.get('endpoints', []):
|
|
if (endpoint['type'] == 'network' and
|
|
endpoint.get('region') == self.region_name):
|
|
if self.endpoint_type not in endpoint:
|
|
raise exceptions.EndpointTypeNotFound(
|
|
message=self.endpoint_type)
|
|
return endpoint[self.endpoint_type]
|
|
|
|
raise exceptions.EndpointNotFound()
|
|
|
|
def get_auth_info(self):
|
|
return {'auth_token': self.auth_token,
|
|
'auth_tenant_id': self.auth_tenant_id,
|
|
'auth_user_id': self.auth_user_id,
|
|
'endpoint_url': self.endpoint_url}
|
|
|
|
def get_status_code(self, response):
|
|
"""Returns the integer status code from the response.
|
|
|
|
Either a Webob.Response (used in testing) or httplib.Response
|
|
is returned.
|
|
"""
|
|
if hasattr(response, 'status_int'):
|
|
return response.status_int
|
|
else:
|
|
return response.status
|