Use client credentials to retrieve service list

Credentials received in the request are used to retrieve endpoint
list from keystone. This avoids the usage of the admin creds
and opens the gate towards connecting to any clouds without
previous configuration.

Implements: blueprint mistral-multi-vim-support
Change-Id: Ib5ae5911f2535f4f340af8f4bcb4817818747029
This commit is contained in:
Andras Kovi 2016-06-08 17:26:19 +02:00
parent 49e9486617
commit 998f5158e1
4 changed files with 118 additions and 26 deletions

View File

@ -109,21 +109,24 @@ class KeystoneAction(base.OpenStackAction):
LOG.debug("Keystone action security context: %s" % ctx) LOG.debug("Keystone action security context: %s" % ctx)
# TODO(akovi) cacert is deprecated in favor of session
# TODO(akovi) this piece of code should be refactored
# TODO(akovi) to follow the new guide lines
kwargs = { kwargs = {
'token': ctx.auth_token, 'token': ctx.auth_token,
'auth_url': CONF.keystone_authtoken.auth_uri, 'auth_url': ctx.auth_uri,
'project_id': ctx.project_id, 'project_id': ctx.project_id,
'cacert': CONF.keystone_authtoken.cafile, 'cacert': ctx.auth_cacert,
} }
# In case of trust-scoped token explicitly pass endpoint parameter. # In case of trust-scoped token explicitly pass endpoint parameter.
if (ctx.is_trust_scoped if (ctx.is_trust_scoped
or keystone_utils.is_token_trust_scoped(ctx.auth_token)): or keystone_utils.is_token_trust_scoped(ctx.auth_token)):
kwargs['endpoint'] = CONF.keystone_authtoken.auth_uri kwargs['endpoint'] = ctx.auth_uri
client = self._client_class(**kwargs) client = self._client_class(**kwargs)
client.management_url = CONF.keystone_authtoken.auth_uri client.management_url = ctx.auth_uri
return client return client
@ -214,7 +217,7 @@ class NeutronAction(base.OpenStackAction):
endpoint_url=neutron_endpoint.url, endpoint_url=neutron_endpoint.url,
region_name=neutron_endpoint.region, region_name=neutron_endpoint.region,
token=ctx.auth_token, token=ctx.auth_token,
auth_url=CONF.keystone_authtoken.auth_uri auth_url=ctx.auth_uri
) )
@ -595,7 +598,7 @@ class DesignateAction(base.OpenStackAction):
client = self._client_class( client = self._client_class(
endpoint=designate_url, endpoint=designate_url,
tenant_id=ctx.project_id, tenant_id=ctx.project_id,
auth_url=CONF.keystone_authtoken.auth_uri, auth_url=ctx.auth_uri,
region_name=designate_endpoint.region, region_name=designate_endpoint.region,
service_type='dns' service_type='dns'
) )

View File

@ -68,6 +68,8 @@ class BaseContext(object):
class MistralContext(BaseContext): class MistralContext(BaseContext):
# Use set([...]) since set literals are not supported in Python 2.6. # Use set([...]) since set literals are not supported in Python 2.6.
_elements = set([ _elements = set([
"auth_uri",
"auth_cacert",
"user_id", "user_id",
"project_id", "project_id",
"auth_token", "auth_token",
@ -120,6 +122,8 @@ def spawn(thread_description, func, *args, **kwargs):
def context_from_headers(headers): def context_from_headers(headers):
return MistralContext( return MistralContext(
auth_uri=CONF.keystone_authtoken.auth_uri,
auth_cacert=CONF.keystone_authtoken.cafile,
user_id=headers.get('X-User-Id'), user_id=headers.get('X-User-Id'),
project_id=headers.get('X-Project-Id'), project_id=headers.get('X-Project-Id'),
auth_token=headers.get('X-Auth-Token'), auth_token=headers.get('X-Auth-Token'),

View File

@ -15,6 +15,57 @@
from mistral.tests.unit import base from mistral.tests.unit import base
from mistral.utils.openstack import keystone from mistral.utils.openstack import keystone
SERVICES_CATALOG = [
{
"type": "compute",
"name": "nova",
"endpoints": [
{
"interface": "private",
"url": "https://example.com/nova/private",
"region": "RegionOne"
},
{
"interface": "public",
"url": "https://example.com/nova/public",
"region": "RegionOne"
}
]
},
{
"type": "compute",
"name": "nova2",
"endpoints": [
{
"interface": "public",
"url": "https://example.com/nova2/public/r1",
"region": "RegionOne"
},
{
"interface": "public",
"url": "https://example.com/nova2/public/r2",
"region": "RegionTwo"
}
]
},
{
"type": "orchestration",
"name": "heat",
"endpoints": [
{
"interface": "private",
"url": "https://example.com/heat/private",
"region": "RegionOne"
},
{
"interface": "public",
"url": "https://example.com/heat/public",
"region": "RegionOne"
}
]
}
]
class KeystoneUtilsTest(base.BaseTest): class KeystoneUtilsTest(base.BaseTest):
def setUp(self): def setUp(self):
@ -41,3 +92,24 @@ class KeystoneUtilsTest(base.BaseTest):
expected, expected,
keystone.format_url(url_template, self.values) keystone.format_url(url_template, self.values)
) )
def test_service_endpoints_select(self):
def find(name, typ=None, catalog=SERVICES_CATALOG):
return keystone.select_service_endpoints(name, typ, catalog)
endpoints = find('nova', 'compute')
self.assertEqual('https://example.com/nova/public', endpoints[0].url,
message='public interface must be selected')
endpoints = find('nova2')
self.assertEqual(2, len(endpoints),
message='public endpoints must be selected '
'in each region')
endpoints = find('heat')
self.assertEqual('https://example.com/heat/public', endpoints[0].url,
message='selection should work without type set')
endpoints = find('nova', None, [])
self.assertEqual([], endpoints,
message='empty catalog should be accepted')

View File

@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from keystoneclient.v3 import client as ks_client from keystoneclient.v3 import client as ks_client
from keystoneclient.v3.endpoints import Endpoint
from oslo_config import cfg from oslo_config import cfg
from mistral import context from mistral import context
@ -23,7 +24,7 @@ CONF = cfg.CONF
def client(): def client():
ctx = context.ctx() ctx = context.ctx()
auth_url = CONF.keystone_authtoken.auth_uri auth_url = ctx.auth_uri
cl = ks_client.Client( cl = ks_client.Client(
username=ctx.user_name, username=ctx.user_name,
@ -62,37 +63,49 @@ def client_for_trusts(trust_id):
def get_endpoint_for_project(service_name=None, service_type=None): def get_endpoint_for_project(service_name=None, service_type=None):
admin_project_name = CONF.keystone_authtoken.admin_tenant_name if service_name is None and service_type is None:
keystone_client = _admin_client(project_name=admin_project_name)
service_list = keystone_client.services.list()
if service_name:
service_ids = [s.id for s in service_list if s.name == service_name]
elif service_type:
service_ids = [s.id for s in service_list if s.type == service_type]
else:
raise Exception( raise Exception(
"Either 'service_name' or 'service_type' must be provided." "Either 'service_name' or 'service_type' must be provided."
) )
if not service_ids: ctx = context.ctx()
raise Exception("Either service '%s' or service type "
"'%s' doesn't exist!" % (service_name, service_type))
endpoints = keystone_client.endpoints.list( token = ctx.auth_token
service=service_ids[0], response = client().tokens.get_token_data(token, include_catalog=True)
interface='public'
) endpoints = select_service_endpoints(
service_name,
service_type,
response["token"]["catalog"])
if not endpoints: if not endpoints:
raise Exception( raise Exception(
"No endpoints found [service_name=%s, service_type=%s]" "No endpoints found [service_name=%s, service_type=%s]"
% (service_name, service_type) % (service_name, service_type)
) )
else:
# TODO(rakhmerov): We may have more than one endpoint because
# TODO(rakhmerov): of regions and ideally we need a config option
# TODO(rakhmerov): for region
return endpoints[0]
# TODO(rakhmerov): We may have more than one endpoint because of regions
# TODO(rakhmerov): and ideally we need a config option for region def select_service_endpoints(service_name, service_type, services):
return endpoints[0] endpoints = []
for catalog in services:
if service_name and catalog["name"] != service_name:
continue
if service_type and catalog["type"] != service_type:
continue
for endpoint in catalog["endpoints"]:
if endpoint["interface"] == 'public':
endpoints.append(Endpoint(None, endpoint, loaded=True))
return endpoints
def get_keystone_endpoint_v2(): def get_keystone_endpoint_v2():