Support remote stack with another OpenStack provider
Allow OS::Heat::Stack to access remote stack from another OpenStack provider. Also enable functional tests for multi-cloud. Implement multi-cloud support as an extension to the existing multi-region support. Allow operate a remote stack (from another OpenStack cloud) as a resource in stack from local OpenStack cloud. I propose we add multi cloud support into ``OS::Heat::Stack`` and change the property schema for ``context``. Within context, we should adding following properties: * credential_secret_id: ID of Barbican Secret. Which stores authN information for remote cloud. Service will use auth information from Barbican Secret to access Orchestration service in another OpenStack. Must make sure you're able toget that secret from Barbican service when provide `credential_secret_id` property. Story: #2002126 Task: #26907 Depends-On: https://review.openstack.org/579750 Change-Id: I2f3de3e7c29cf7debb1474228c8a9a81725a72ed
This commit is contained in:
parent
6c35f28365
commit
6990331639
@ -14,6 +14,7 @@
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from heat.common import auth_plugin
|
||||
from heat.common import context
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
@ -22,6 +23,7 @@ from heat.engine import attributes
|
||||
from heat.engine import environment
|
||||
from heat.engine import properties
|
||||
from heat.engine import resource
|
||||
from heat.engine import support
|
||||
from heat.engine import template
|
||||
|
||||
|
||||
@ -48,21 +50,31 @@ class RemoteStack(resource.Resource):
|
||||
)
|
||||
|
||||
_CONTEXT_KEYS = (
|
||||
REGION_NAME
|
||||
REGION_NAME, CREDENTIAL_SECRET_ID
|
||||
) = (
|
||||
'region_name'
|
||||
'region_name', 'credential_secret_id'
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
CONTEXT: properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Context for this stack.'),
|
||||
update_allowed=True,
|
||||
schema={
|
||||
REGION_NAME: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Region name in which this stack will be created.'),
|
||||
required=True,
|
||||
)
|
||||
required=False,
|
||||
),
|
||||
CREDENTIAL_SECRET_ID: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('A Barbican secret ID. The Barbican secret should '
|
||||
'contain an OpenStack credential that can be used to '
|
||||
'access a remote cloud.'),
|
||||
required=False,
|
||||
update_allowed=True,
|
||||
support_status=support.SupportStatus(version='12.0.0'),
|
||||
),
|
||||
}
|
||||
),
|
||||
TEMPLATE: properties.Schema(
|
||||
@ -107,10 +119,45 @@ class RemoteStack(resource.Resource):
|
||||
|
||||
ctx_props = self.properties.get(self.CONTEXT)
|
||||
if ctx_props:
|
||||
self._region_name = ctx_props[self.REGION_NAME]
|
||||
self._credential = ctx_props[self.CREDENTIAL_SECRET_ID]
|
||||
self._region_name = ctx_props[self.REGION_NAME] if ctx_props[
|
||||
self.REGION_NAME] else self.context.region_name
|
||||
else:
|
||||
self._credential = None
|
||||
self._region_name = self.context.region_name
|
||||
|
||||
if ctx_props and self._credential:
|
||||
return self._prepare_cloud_context()
|
||||
else:
|
||||
return self._prepare_region_context()
|
||||
|
||||
def _fetch_barbican_credential(self):
|
||||
"""Fetch credential information and return context dict."""
|
||||
|
||||
auth = super(RemoteStack, self).client_plugin(
|
||||
'barbican').get_secret_payload_by_ref(
|
||||
secret_ref='secrets/%s' % (self._credential))
|
||||
return auth
|
||||
|
||||
def _prepare_cloud_context(self):
|
||||
"""Prepare context for remote cloud."""
|
||||
|
||||
auth = self._fetch_barbican_credential()
|
||||
dict_ctxt = self.context.to_dict()
|
||||
dict_ctxt.update({
|
||||
'request_id': dict_ctxt['request_id'],
|
||||
'global_request_id': dict_ctxt['global_request_id'],
|
||||
'show_deleted': dict_ctxt['show_deleted']
|
||||
})
|
||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
||||
self._local_context._auth_plugin = (
|
||||
auth_plugin.get_keystone_plugin_loader(
|
||||
auth, self._local_context.keystone_session))
|
||||
|
||||
return self._local_context
|
||||
|
||||
def _prepare_region_context(self):
|
||||
|
||||
# Build RequestContext from existing one
|
||||
dict_ctxt = self.context.to_dict()
|
||||
dict_ctxt.update({'region_name': self._region_name,
|
||||
@ -132,9 +179,13 @@ class RemoteStack(resource.Resource):
|
||||
try:
|
||||
self.heat()
|
||||
except Exception as ex:
|
||||
exc_info = dict(region=self._region_name, exc=six.text_type(ex))
|
||||
msg = _('Cannot establish connection to Heat endpoint at region '
|
||||
'"%(region)s" due to "%(exc)s"') % exc_info
|
||||
if self._credential:
|
||||
location = "remote cloud"
|
||||
else:
|
||||
location = 'region "%s"' % self._region_name
|
||||
exc_info = dict(location=location, exc=six.text_type(ex))
|
||||
msg = _('Cannot establish connection to Heat endpoint at '
|
||||
'%(location)s due to "%(exc)s"') % exc_info
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
try:
|
||||
@ -148,9 +199,13 @@ class RemoteStack(resource.Resource):
|
||||
}
|
||||
self.heat().stacks.validate(**args)
|
||||
except Exception as ex:
|
||||
exc_info = dict(region=self._region_name, exc=six.text_type(ex))
|
||||
if self._credential:
|
||||
location = "remote cloud"
|
||||
else:
|
||||
location = 'region "%s"' % self._region_name
|
||||
exc_info = dict(location=location, exc=six.text_type(ex))
|
||||
msg = _('Failed validating stack template using Heat endpoint at '
|
||||
'region "%(region)s" due to "%(exc)s"') % exc_info
|
||||
'%(location)s due to "%(exc)s"') % exc_info
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
def handle_create(self):
|
||||
@ -304,6 +359,19 @@ class RemoteStack(resource.Resource):
|
||||
def get_reference_id(self):
|
||||
return self.resource_id
|
||||
|
||||
def needs_replace_with_prop_diff(self, changed_properties_set,
|
||||
after_props, before_props):
|
||||
"""Needs replace based on prop_diff."""
|
||||
|
||||
# If region_name changed, trigger UpdateReplace.
|
||||
# `context` now set update_allowed=True, but `region_name` is not.
|
||||
if self.CONTEXT in changed_properties_set and (
|
||||
after_props.get(self.CONTEXT).get(
|
||||
'region_name') != before_props.get(self.CONTEXT).get(
|
||||
'region_name')):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
|
@ -12,16 +12,20 @@
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import json
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import stacks
|
||||
from keystoneauth1 import loading as ks_loading
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common import policy
|
||||
from heat.common import template_format
|
||||
from heat.engine.clients.os import barbican as barbican_client
|
||||
from heat.engine.clients.os import heat_plugin
|
||||
from heat.engine import environment
|
||||
from heat.engine import node_data
|
||||
@ -144,14 +148,18 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
|
||||
self.addCleanup(unset_clients_property)
|
||||
|
||||
def initialize(self):
|
||||
parent, rsrc = self.create_parent_stack(remote_region='RegionTwo')
|
||||
def initialize(self, stack_template=None):
|
||||
parent, rsrc = self.create_parent_stack(remote_region='RegionTwo',
|
||||
stack_template=stack_template)
|
||||
self.parent = parent
|
||||
self.heat = rsrc._context().clients.client("heat")
|
||||
self.client_plugin = rsrc._context().clients.client_plugin('heat')
|
||||
|
||||
def create_parent_stack(self, remote_region=None, custom_template=None):
|
||||
snippet = template_format.parse(parent_stack_template)
|
||||
def create_parent_stack(self, remote_region=None, custom_template=None,
|
||||
stack_template=None):
|
||||
if not stack_template:
|
||||
stack_template = parent_stack_template
|
||||
snippet = template_format.parse(stack_template)
|
||||
self.files = {
|
||||
'remote_template.yaml': custom_template or remote_template
|
||||
}
|
||||
@ -196,13 +204,13 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
|
||||
return parent, rsrc
|
||||
|
||||
def create_remote_stack(self):
|
||||
def create_remote_stack(self, stack_template=None):
|
||||
# This method default creates a stack on RegionTwo (self.other_region)
|
||||
defaults = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
||||
get_stack(stack_status='CREATE_COMPLETE')]
|
||||
|
||||
if self.parent is None:
|
||||
self.initialize()
|
||||
self.initialize(stack_template=stack_template)
|
||||
|
||||
# prepare clients to return status
|
||||
self.heat.stacks.create.return_value = {'stack': get_stack().to_dict()}
|
||||
@ -297,6 +305,58 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
self.heat.stacks.create.assert_called_with(**args)
|
||||
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
||||
|
||||
def _create_with_remote_credential(self, credential_secret_id=None):
|
||||
self.auth = (
|
||||
'{"auth_type": "v3applicationcredential", '
|
||||
'"auth": {"auth_url": "http://192.168.1.101/identity/v3", '
|
||||
'"application_credential_id": "9dfa187e5a354484bf9c49a2b674333a", '
|
||||
'"application_credential_secret": "sec"} }')
|
||||
|
||||
t = template_format.parse(parent_stack_template)
|
||||
properties = t['resources']['remote_stack']['properties']
|
||||
if credential_secret_id:
|
||||
properties['context']['credential_secret_id'] = (
|
||||
credential_secret_id)
|
||||
t = json.dumps(t)
|
||||
self.patchobject(policy.Enforcer, 'check_is_admin')
|
||||
self.m_gsbr = self.patchobject(
|
||||
barbican_client.BarbicanClientPlugin, 'get_secret_payload_by_ref')
|
||||
self.m_gsbr.return_value = self.auth
|
||||
|
||||
rsrc = self.create_remote_stack(stack_template=t)
|
||||
env = environment.get_child_environment(rsrc.stack.env,
|
||||
{'name': 'foo'})
|
||||
args = {
|
||||
'stack_name': rsrc.physical_resource_name(),
|
||||
'template': template_format.parse(remote_template),
|
||||
'timeout_mins': 60,
|
||||
'disable_rollback': True,
|
||||
'parameters': {'name': 'foo'},
|
||||
'files': self.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
self.heat.stacks.create.assert_called_with(**args)
|
||||
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
||||
rsrc.validate()
|
||||
return rsrc
|
||||
|
||||
def test_create_with_credential_secret_id(self):
|
||||
self.m_plugin = mock.Mock()
|
||||
self.m_loader = self.patchobject(
|
||||
ks_loading, 'get_plugin_loader', return_value=self.m_plugin)
|
||||
self._create_with_remote_credential('cred_2')
|
||||
self.assertEqual(
|
||||
[mock.call(secret_ref='secrets/cred_2')]*2,
|
||||
self.m_gsbr.call_args_list)
|
||||
expected_load_options = [
|
||||
mock.call(
|
||||
application_credential_id='9dfa187e5a354484bf9c49a2b674333a',
|
||||
application_credential_secret='sec',
|
||||
auth_url='http://192.168.1.101/identity/v3')]*2
|
||||
|
||||
self.assertEqual(expected_load_options,
|
||||
self.m_plugin.load_from_options.call_args_list)
|
||||
|
||||
def test_create_failed(self):
|
||||
returns = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
||||
get_stack(stack_status='CREATE_FAILED',
|
||||
|
@ -80,6 +80,13 @@ function _config_tempest_plugin
|
||||
iniset $conf_file heat_plugin heat_config_notify_script $CONF_DEST/heat-agents/heat-config/bin/heat-config-notify
|
||||
iniset $conf_file heat_plugin boot_config_env $CONF_DEST/heat-templates/hot/software-config/boot-config/test_image_env.yaml
|
||||
|
||||
# support test multi-cloud
|
||||
openstack application credential create heat_multicloud --secret secret --unrestricted
|
||||
app_cred_id=$(openstack application credential show heat_multicloud|grep ' id '|awk '{print $4}')
|
||||
export OS_CREDENTIAL_SECRET_ID=$(openstack secret store -n heat-multi-cloud-test-cred --payload '{"auth_type": "v3applicationcredential", "auth": {"auth_url": $OS_AUTH_URL, "application_credential_id": $app_cred_id, "application_credential_secret": "secret"}, "roles": ["admin"], "project_id": $app_cred_project_id}')
|
||||
iniset $conf_file heat_features_enabled multi_cloud True
|
||||
iniset $conf_file heat_plugin heat_plugin credential_secret_id $OS_CREDENTIAL_SECRET_ID
|
||||
|
||||
# Skip SoftwareConfigIntegrationTest because it requires a custom image
|
||||
# Skip VolumeBackupRestoreIntegrationTest skipped until failure rate can be reduced ref bug #1382300
|
||||
# Skip AutoscalingLoadBalancerTest and AutoscalingLoadBalancerv2Test as deprecated neutron-lbaas service is not enabled
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add multiple OpenStack orchestration support - User can now use
|
||||
``OS::Heat::Stack`` to create stack in another OpenStack cloud.
|
||||
Must provide properties ``credential_secret_id`` in ``context``.
|
||||
Remote stack resource will get authentication information from
|
||||
cloud credential to refresh context before calling stack create.
|
Loading…
x
Reference in New Issue
Block a user