Merge "Accept service catalog from client side"

This commit is contained in:
Jenkins 2016-09-30 10:53:19 +00:00 committed by Gerrit Code Review
commit 7cd0119723
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)
)
if not ctx.target_service_catalog:
response = client().tokens.get_token_data(
token,
include_catalog=True)['token']
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
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
# Create stack with client2.
result = _execute_action(client_2, _get_create_stack_request())
stack_id = str(
jsonutils.loads(result['output'])['result']['stack']['id']
)
# 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 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(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])
create_request = {
return {
'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'])
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_request = {
def _get_list_stack_request():
return {
'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)
self.assertEqual(
stack_id,
str(json.loads(body['output'])['result'][0]['id'])
)