Merge "Support remote stack with another OpenStack provider"

This commit is contained in:
Zuul 2019-03-14 02:31:00 +00:00 committed by Gerrit Code Review
commit b055400bd7
4 changed files with 159 additions and 16 deletions

View File

@ -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 {

View File

@ -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',

View File

@ -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

View File

@ -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.