Accept service catalog from client side

Updates the Mistral server to accept the service catalog
from the client request. This enable the server to cooperate
with Keystone Identity V2 and V3 at the same time.

Change-Id: I7ca2aace4d5095828e5053af6965b833109d338a
Closes-Bug: #1612705
Depends-On: I86fa58de00d01c89e4bbc21dbe128f1306e2a1bf
Signed-off-by: Andras Kovi <akovi@nokia.com>
This commit is contained in:
Andras Kovi 2016-08-17 17:37:31 +02:00
parent 75367cc08e
commit 9ebf329aa0
4 changed files with 176 additions and 186 deletions

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
from keystoneclient.v3 import client as keystone_client
import logging
from oslo_config import cfg
@ -26,12 +28,8 @@ from mistral import auth
from mistral import exceptions as exc
from mistral import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
_CTX_THREAD_LOCAL_NAME = "MISTRAL_APP_CTX_THREAD_LOCAL"
ALLOWED_WITHOUT_AUTH = ['/', '/v2/']
@ -78,6 +76,7 @@ class MistralContext(BaseContext):
"project_id",
"auth_token",
"service_catalog",
"target_service_catalog",
"user_name",
"project_name",
"roles",
@ -115,6 +114,7 @@ def context_from_headers_and_env(headers, env):
project_id = params['project_id']
user_id = params['user_id']
user_name = params['user_name']
target_service_catalog = params['target_service_catalog']
token_info = env.get('keystone.token_info')
@ -124,7 +124,7 @@ def context_from_headers_and_env(headers, env):
user_id=user_id,
project_id=project_id,
auth_token=auth_token,
service_catalog=headers.get('X-Service-Catalog'),
target_service_catalog=target_service_catalog,
user_name=user_name,
project_name=headers.get('X-Project-Name'),
roles=headers.get('X-Roles', "").split(","),
@ -134,6 +134,8 @@ def context_from_headers_and_env(headers, env):
def _extract_auth_params_from_headers(headers):
target_service_catalog = None
if headers.get("X-Target-Auth-Uri"):
params = {
# TODO(akovi): Target cert not handled yet
@ -148,6 +150,10 @@ def _extract_auth_params_from_headers(headers):
raise (exc.MistralException(
'Target auth URI (X-Target-Auth-Uri) target auth token '
'(X-Target-Auth-Token) must be present'))
target_service_catalog = _extract_service_catalog_from_headers(
headers
)
else:
params = {
'auth_cacert': CONF.keystone_authtoken.cafile,
@ -157,9 +163,23 @@ def _extract_auth_params_from_headers(headers):
'user_id': headers.get('X-User-Id'),
'user_name': headers.get('X-User-Name'),
}
params['target_service_catalog'] = target_service_catalog
return params
def _extract_service_catalog_from_headers(headers):
target_service_catalog_header = headers.get(
'X-Target-Service-Catalog')
if target_service_catalog_header:
decoded_catalog = base64.b64decode(
target_service_catalog_header).decode()
return jsonutils.loads(decoded_catalog)
else:
return None
def context_from_config():
keystone = keystone_client.Client(
username=CONF.keystone_authtoken.admin_user,

View File

@ -16,72 +16,6 @@ from mistral import config
from mistral.tests.unit import base
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"
},
{
"interface": "internal",
"url": "https://example.com/nova/internal",
"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"
},
{
"interface": "internal",
"url": "https://example.com/nova2/internal",
"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"
},
{
"interface": "internal",
"url": "https://example.com/heat/internal",
"region": "RegionTwo"
}
]
}
]
class KeystoneUtilsTest(base.BaseTest):
def setUp(self):
@ -112,43 +46,3 @@ class KeystoneUtilsTest(base.BaseTest):
expected,
keystone.format_url(url_template, self.values)
)
def test_service_endpoints_select_default(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')
def test_service_endpoints_select_internal(self):
def find(name, typ=None, catalog=SERVICES_CATALOG):
return keystone.select_service_endpoints(name, typ, catalog)
self.override_config('os_actions_endpoint_type', 'internal')
endpoints = find('nova', 'compute')
self.assertEqual('https://example.com/nova/internal', endpoints[0].url,
message='internal interface must be selected')
endpoints = find('nova2')
self.assertEqual("https://example.com/nova2/internal",
endpoints[0].url,
message='internal endpoints must be selected '
'in each region')
endpoints = find('heat')
self.assertEqual('https://example.com/heat/internal', endpoints[0].url,
message='selection should work without type set')

View File

@ -15,8 +15,9 @@
import keystoneauth1.identity.generic as auth_plugins
from keystoneauth1 import session as ks_session
from keystoneclient import service_catalog as ks_service_catalog
from keystoneclient.v3 import client as ks_client
from keystoneclient.v3 import endpoints as enp
from keystoneclient.v3 import endpoints as ks_endpoints
from oslo_config import cfg
from oslo_utils import timeutils
@ -73,9 +74,55 @@ def get_endpoint_for_project(service_name=None, service_type=None):
ctx = context.ctx()
token = ctx.auth_token
service_catalog = obtain_service_catalog(ctx)
if (ctx.is_trust_scoped and is_token_trust_scoped(token)):
catalog = service_catalog.get_endpoints(
service_name=service_name,
service_type=service_type
)
endpoint = None
for service_type in catalog:
service = catalog.get(service_type)
for interface in service:
# is V3 interface?
if 'interface' in interface:
interface_type = interface['interface']
if CONF.os_actions_endpoint_type in interface_type:
endpoint = ks_endpoints.Endpoint(
None,
interface,
loaded=True
)
break
# is V2 interface?
if 'publicURL' in interface:
endpoint_data = {
'url': interface['publicURL'],
'region': interface['region']
}
endpoint = ks_endpoints.Endpoint(
None,
endpoint_data,
loaded=True
)
break
if not endpoint:
raise Exception(
"No endpoints found [service_name=%s, service_type=%s]"
% (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 endpoint
def obtain_service_catalog(ctx):
token = ctx.auth_token
if ctx.is_trust_scoped and is_token_trust_scoped(token):
if ctx.trust_id is None:
raise Exception(
"'trust_id' must be provided in the admin context."
@ -85,45 +132,16 @@ def get_endpoint_for_project(service_name=None, service_type=None):
response = trust_client.tokens.get_token_data(
token,
include_catalog=True
)
)['token']
else:
response = client().tokens.get_token_data(token, include_catalog=True)
endpoints = select_service_endpoints(
service_name,
service_type,
response["token"]["catalog"])
if not endpoints:
raise Exception(
"No endpoints found [service_name=%s, service_type=%s]"
% (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]
def select_service_endpoints(service_name, service_type, services):
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"]:
# Keystone v2.0 uses <interface>URL while v3 only uses the
# interface without the URL suffix.
if CONF.os_actions_endpoint_type in endpoint["interface"]:
endpoints.append(enp.Endpoint(None, endpoint, loaded=True))
return endpoints
if not ctx.target_service_catalog:
response = client().tokens.get_token_data(
token,
include_catalog=True)['token']
else:
response = ctx.target_service_catalog
service_catalog = ks_service_catalog.ServiceCatalog.factory(response)
return service_catalog
def get_keystone_endpoint_v2():
@ -167,6 +185,8 @@ def get_admin_session():
def will_expire_soon(expires_at):
if not expires_at:
return False
stale_duration = CONF.expiration_token_duration
assert stale_duration, "expiration_token_duration must be specified"
expires = timeutils.parse_isotime(expires_at)

View File

@ -11,12 +11,15 @@
# 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.
import json
from mistral_tempest_tests.tests import base
from tempest import test
import base64
from urlparse import urlparse
import uuid
from oslo_serialization import jsonutils
from tempest import test
from mistral_tempest_tests.tests import base
class MultiVimActionsTests(base.TestCase):
_service = 'workflowv2'
@ -26,42 +29,95 @@ class MultiVimActionsTests(base.TestCase):
super(MultiVimActionsTests, cls).resource_setup()
@test.attr(type='openstack')
def test_multi_vim_support(self):
def test_multi_vim_support_target_headers(self):
client_1 = self.alt_client
client_2 = self.client
stack_name = 'multi_vim_test_stack_{}'.format(str(uuid.uuid4())[:8])
create_request = {
'name': 'heat.stacks_create',
'input': {
'stack_name': stack_name,
"template": {"heat_template_version": "2013-05-23"}
}
}
_, body = client_2.create_action_execution(create_request)
stack_id = str(json.loads(body['output'])['result']['stack']['id'])
# Create stack with client2.
result = _execute_action(client_2, _get_create_stack_request())
stack_id = str(
jsonutils.loads(result['output'])['result']['stack']['id']
)
u = urlparse(client_2.auth_provider.auth_url)
v3_auth_url = '{}://{}/identity/v3/'.format(u.scheme, u.netloc)
extra_headers = {
'X-Target-Auth-Token': client_2.token,
'X-Target-Auth-Uri': v3_auth_url,
'X-Target-Project-Id': client_2.tenant_id,
'X-Target-User-Id': client_2.user_id,
'X-Target-User-Name': client_2.user,
}
# List stacks with client1, and assert that there is no stack.
result = _execute_action(client_1, _get_list_stack_request())
self.assertEmpty(jsonutils.loads(result['output'])['result'])
list_request = {
'name': 'heat.stacks_list',
}
_, body = client_1.create_action_execution(list_request)
self.assertEmpty(json.loads(body['output'])['result'])
_, body = client_1.create_action_execution(list_request,
extra_headers=extra_headers)
# List stacks with client1, but with the target headers of client2,
# and assert the created stack is there.
result = _execute_action(
client_1,
_get_list_stack_request(),
extra_headers=_extract_target_headers_from_client(client_2)
)
self.assertEqual(
stack_id,
str(json.loads(body['output'])['result'][0]['id'])
str(jsonutils.loads(result['output'])['result'][0]['id'])
)
@test.attr(type='openstack')
def test_multi_vim_support_target_headers_and_service_catalog(self):
client_1 = self.alt_client
client_2 = self.client
# List stacks with client1, but with the target headers of client2,
# and additionally with an invalid X-Target-Service-Catalog.
extra_headers = _extract_target_headers_from_client(client_2)
service_dict = dict(client_2.auth_provider.cache[1])
for endpoint in service_dict['serviceCatalog']:
if endpoint['name'] == 'heat':
endpoint['endpoints'][0]['publicURL'] = "invalid"
service_catalog = {
"X-Target-Service-Catalog": base64.b64encode(
jsonutils.dumps(service_dict)
)
}
extra_headers.update(service_catalog)
result = _execute_action(
client_1,
_get_list_stack_request(),
extra_headers=extra_headers
)
# Assert that the invalid catalog was used.
self.assertIn("Invalid URL", result['output'])
def _extract_target_headers_from_client(client):
u = urlparse(client.auth_provider.auth_url)
v3_auth_url = '{}://{}/identity/v3/'.format(u.scheme, u.netloc)
return {
'X-Target-Auth-Token': client.token,
'X-Target-Auth-Uri': v3_auth_url,
'X-Target-Project-Id': client.tenant_id,
'X-Target-User-Id': client.user_id,
}
def _execute_action(client, request, extra_headers={}):
_, result = client.create_action_execution(
request,
extra_headers=extra_headers
)
return result
def _get_create_stack_request():
stack_name = 'multi_vim_test_stack_{}'.format(str(uuid.uuid4())[:8])
return {
'name': 'heat.stacks_create',
'input': {
'stack_name': stack_name,
"template": {"heat_template_version": "2013-05-23"}
}
}
def _get_list_stack_request():
return {
'name': 'heat.stacks_list',
}