Merge "Accept service catalog from client side"
This commit is contained in:
commit
7cd0119723
@ -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