Merge "Support remote stack with another OpenStack provider"
This commit is contained in:
commit
b055400bd7
@ -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…
Reference in New Issue
Block a user