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:
parent
75367cc08e
commit
9ebf329aa0
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user