Add a preview endpoint for stack updates
Allow users to see what resources will be changed during a stack-update. Docs change here https://review.openstack.org/132870/ Client change here https://review.openstack.org/#/c/126957/ BP: update-dry-run Co-Authored-By: Jason Dunsmore <jasondunsmore@gmail.com> Change-Id: If58bdcccfef6f5d36c0367c5267f95014232015e
This commit is contained in:
parent
2dbcd9064d
commit
6513d3944c
@ -53,6 +53,7 @@
|
|||||||
"stacks:show": "rule:deny_stack_user",
|
"stacks:show": "rule:deny_stack_user",
|
||||||
"stacks:template": "rule:deny_stack_user",
|
"stacks:template": "rule:deny_stack_user",
|
||||||
"stacks:update": "rule:deny_stack_user",
|
"stacks:update": "rule:deny_stack_user",
|
||||||
|
"stacks:preview_update": "rule:deny_stack_user",
|
||||||
"stacks:update_patch": "rule:deny_stack_user",
|
"stacks:update_patch": "rule:deny_stack_user",
|
||||||
"stacks:validate_template": "rule:deny_stack_user",
|
"stacks:validate_template": "rule:deny_stack_user",
|
||||||
"stacks:snapshot": "rule:deny_stack_user",
|
"stacks:snapshot": "rule:deny_stack_user",
|
||||||
|
@ -208,6 +208,12 @@ class API(wsgi.Router):
|
|||||||
'action': 'update_patch',
|
'action': 'update_patch',
|
||||||
'method': 'PATCH'
|
'method': 'PATCH'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'preview_stack_update',
|
||||||
|
'url': '/stacks/{stack_name}/{stack_id}/preview',
|
||||||
|
'action': 'preview_update',
|
||||||
|
'method': 'PUT'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'stack_delete',
|
'name': 'stack_delete',
|
||||||
'url': '/stacks/{stack_name}/{stack_id}',
|
'url': '/stacks/{stack_name}/{stack_id}',
|
||||||
|
@ -458,6 +458,24 @@ class StackController(object):
|
|||||||
|
|
||||||
raise exc.HTTPAccepted()
|
raise exc.HTTPAccepted()
|
||||||
|
|
||||||
|
@util.identified_stack
|
||||||
|
def preview_update(self, req, identity, body):
|
||||||
|
"""
|
||||||
|
Preview an update to an existing stack with a new template/parameters
|
||||||
|
"""
|
||||||
|
data = InstantiationData(body)
|
||||||
|
|
||||||
|
args = self.prepare_args(data)
|
||||||
|
changes = self.rpc_client.preview_update_stack(
|
||||||
|
req.context,
|
||||||
|
identity,
|
||||||
|
data.template(),
|
||||||
|
data.environment(),
|
||||||
|
data.files(),
|
||||||
|
args)
|
||||||
|
|
||||||
|
return {'resource_changes': changes}
|
||||||
|
|
||||||
@util.identified_stack
|
@util.identified_stack
|
||||||
def delete(self, req, identity):
|
def delete(self, req, identity):
|
||||||
"""
|
"""
|
||||||
|
@ -865,8 +865,11 @@ class Resource(object):
|
|||||||
resource_data.get('metadata'))
|
resource_data.get('metadata'))
|
||||||
|
|
||||||
def _needs_update(self, after, before, after_props, before_props,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource, check_init_complete=True):
|
||||||
if self.status == self.FAILED or \
|
if self.status == self.FAILED:
|
||||||
|
raise UpdateReplace(self)
|
||||||
|
|
||||||
|
if check_init_complete and \
|
||||||
(self.action == self.INIT and self.status == self.COMPLETE):
|
(self.action == self.INIT and self.status == self.COMPLETE):
|
||||||
raise UpdateReplace(self)
|
raise UpdateReplace(self)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class NoneResource(resource.Resource):
|
|||||||
attributes_schema = {}
|
attributes_schema = {}
|
||||||
|
|
||||||
def _needs_update(self, after, before, after_props, before_props,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource, check_init_complete=True):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def reparse(self):
|
def reparse(self):
|
||||||
|
@ -212,7 +212,7 @@ class RemoteStack(resource.Resource):
|
|||||||
self.heat().actions.check(stack_id=self.resource_id)
|
self.heat().actions.check(stack_id=self.resource_id)
|
||||||
|
|
||||||
def _needs_update(self, after, before, after_props, before_props,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource, check_init_complete=True):
|
||||||
# Always issue an update to the remote stack and let the individual
|
# Always issue an update to the remote stack and let the individual
|
||||||
# resources in it decide if they need updating.
|
# resources in it decide if they need updating.
|
||||||
return True
|
return True
|
||||||
|
@ -413,13 +413,14 @@ class Port(neutron.NeutronResource):
|
|||||||
return super(Port, self)._resolve_attribute(name)
|
return super(Port, self)._resolve_attribute(name)
|
||||||
|
|
||||||
def _needs_update(self, after, before, after_props, before_props,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource, check_init_complete=True):
|
||||||
|
|
||||||
if after_props.get(self.REPLACEMENT_POLICY) == 'REPLACE_ALWAYS':
|
if after_props.get(self.REPLACEMENT_POLICY) == 'REPLACE_ALWAYS':
|
||||||
raise resource.UpdateReplace(self.name)
|
raise resource.UpdateReplace(self.name)
|
||||||
|
|
||||||
return super(Port, self)._needs_update(
|
return super(Port, self)._needs_update(
|
||||||
after, before, after_props, before_props, prev_resource)
|
after, before, after_props, before_props, prev_resource,
|
||||||
|
check_init_complete)
|
||||||
|
|
||||||
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
||||||
props = self.prepare_update_properties(json_snippet)
|
props = self.prepare_update_properties(json_snippet)
|
||||||
|
@ -86,16 +86,18 @@ class StackResource(resource.Resource):
|
|||||||
self._resolve_all_attributes)
|
self._resolve_all_attributes)
|
||||||
|
|
||||||
def _needs_update(self, after, before, after_props, before_props,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource, check_init_complete=True):
|
||||||
# Issue an update to the nested stack if the stack resource
|
# Issue an update to the nested stack if the stack resource
|
||||||
# is able to update. If return true, let the individual
|
# is able to update. If return true, let the individual
|
||||||
# resources in it decide if they need updating.
|
# resources in it decide if they need updating.
|
||||||
|
|
||||||
# FIXME (ricolin): seems currently can not call super here
|
# FIXME (ricolin): seems currently can not call super here
|
||||||
if self.nested() is None and (
|
if self.nested() is None and self.status == self.FAILED:
|
||||||
self.status == self.FAILED
|
raise resource.UpdateReplace(self)
|
||||||
or (self.action == self.INIT
|
|
||||||
and self.status == self.COMPLETE)):
|
if (check_init_complete and
|
||||||
|
self.nested() is None and
|
||||||
|
self.action == self.INIT and self.status == self.COMPLETE):
|
||||||
raise resource.UpdateReplace(self)
|
raise resource.UpdateReplace(self)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -57,6 +57,7 @@ from heat.engine import stack as parser
|
|||||||
from heat.engine import stack_lock
|
from heat.engine import stack_lock
|
||||||
from heat.engine import support
|
from heat.engine import support
|
||||||
from heat.engine import template as templatem
|
from heat.engine import template as templatem
|
||||||
|
from heat.engine import update
|
||||||
from heat.engine import watchrule
|
from heat.engine import watchrule
|
||||||
from heat.engine import worker
|
from heat.engine import worker
|
||||||
from heat.objects import event as event_object
|
from heat.objects import event as event_object
|
||||||
@ -272,7 +273,7 @@ class EngineService(service.Service):
|
|||||||
by the RPC caller.
|
by the RPC caller.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RPC_API_VERSION = '1.14'
|
RPC_API_VERSION = '1.15'
|
||||||
|
|
||||||
def __init__(self, host, topic):
|
def __init__(self, host, topic):
|
||||||
super(EngineService, self).__init__()
|
super(EngineService, self).__init__()
|
||||||
@ -717,6 +718,46 @@ class EngineService(service.Service):
|
|||||||
|
|
||||||
return dict(stack.identifier())
|
return dict(stack.identifier())
|
||||||
|
|
||||||
|
def _prepare_stack_updates(self, cnxt, current_stack, tmpl, params,
|
||||||
|
files, args):
|
||||||
|
"""
|
||||||
|
Given a stack and update context, return the current and updated stack.
|
||||||
|
|
||||||
|
Changes *will not* be persisted, this is a helper method for
|
||||||
|
update_stack and preview_update_stack.
|
||||||
|
|
||||||
|
:param cnxt: RPC context.
|
||||||
|
:param stack: A stack to be updated.
|
||||||
|
:param tmpl: Template object of stack you want to update to.
|
||||||
|
:param params: Stack Input Params
|
||||||
|
:param files: Files referenced from the template
|
||||||
|
:param args: Request parameters/args passed from API
|
||||||
|
"""
|
||||||
|
max_resources = cfg.CONF.max_resources_per_stack
|
||||||
|
if max_resources != -1 and len(tmpl[tmpl.RESOURCES]) > max_resources:
|
||||||
|
raise exception.RequestLimitExceeded(
|
||||||
|
message=exception.StackResourceLimitExceeded.msg_fmt)
|
||||||
|
|
||||||
|
stack_name = current_stack.name
|
||||||
|
current_kwargs = current_stack.get_kwargs_for_cloning()
|
||||||
|
|
||||||
|
common_params = api.extract_args(args)
|
||||||
|
common_params.setdefault(rpc_api.PARAM_TIMEOUT,
|
||||||
|
current_stack.timeout_mins)
|
||||||
|
common_params.setdefault(rpc_api.PARAM_DISABLE_ROLLBACK,
|
||||||
|
current_stack.disable_rollback)
|
||||||
|
|
||||||
|
current_kwargs.update(common_params)
|
||||||
|
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
||||||
|
**current_kwargs)
|
||||||
|
self.resource_enforcer.enforce_stack(updated_stack)
|
||||||
|
updated_stack.parameters.set_stack_id(current_stack.identifier())
|
||||||
|
|
||||||
|
self._validate_deferred_auth_context(cnxt, updated_stack)
|
||||||
|
updated_stack.validate()
|
||||||
|
|
||||||
|
return current_stack, updated_stack
|
||||||
|
|
||||||
@context.request_context
|
@context.request_context
|
||||||
def update_stack(self, cnxt, stack_identity, template, params,
|
def update_stack(self, cnxt, stack_identity, template, params,
|
||||||
files, args):
|
files, args):
|
||||||
@ -767,29 +808,11 @@ class EngineService(service.Service):
|
|||||||
new_env = environment.Environment(params)
|
new_env = environment.Environment(params)
|
||||||
new_files = files
|
new_files = files
|
||||||
tmpl = templatem.Template(template, files=new_files, env=new_env)
|
tmpl = templatem.Template(template, files=new_files, env=new_env)
|
||||||
max_resources = cfg.CONF.max_resources_per_stack
|
|
||||||
if max_resources != -1 and len(tmpl[tmpl.RESOURCES]) > max_resources:
|
|
||||||
raise exception.RequestLimitExceeded(
|
|
||||||
message=exception.StackResourceLimitExceeded.msg_fmt)
|
|
||||||
stack_name = current_stack.name
|
|
||||||
current_kwargs = current_stack.get_kwargs_for_cloning()
|
|
||||||
|
|
||||||
common_params = api.extract_args(args)
|
current_stack, updated_stack = self._prepare_stack_updates(
|
||||||
common_params.setdefault(rpc_api.PARAM_TIMEOUT,
|
cnxt, current_stack, tmpl, params, files, args)
|
||||||
current_stack.timeout_mins)
|
|
||||||
common_params.setdefault(rpc_api.PARAM_DISABLE_ROLLBACK,
|
|
||||||
current_stack.disable_rollback)
|
|
||||||
|
|
||||||
current_kwargs.update(common_params)
|
if current_stack.get_kwargs_for_cloning()['convergence']:
|
||||||
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
|
||||||
**current_kwargs)
|
|
||||||
self.resource_enforcer.enforce_stack(updated_stack)
|
|
||||||
updated_stack.parameters.set_stack_id(current_stack.identifier())
|
|
||||||
|
|
||||||
self._validate_deferred_auth_context(cnxt, updated_stack)
|
|
||||||
updated_stack.validate()
|
|
||||||
|
|
||||||
if current_kwargs['convergence']:
|
|
||||||
current_stack.converge_stack(template=tmpl,
|
current_stack.converge_stack(template=tmpl,
|
||||||
new_stack=updated_stack)
|
new_stack=updated_stack)
|
||||||
else:
|
else:
|
||||||
@ -804,6 +827,57 @@ class EngineService(service.Service):
|
|||||||
self.thread_group_mgr.add_event(current_stack.id, event)
|
self.thread_group_mgr.add_event(current_stack.id, event)
|
||||||
return dict(current_stack.identifier())
|
return dict(current_stack.identifier())
|
||||||
|
|
||||||
|
@context.request_context
|
||||||
|
def preview_update_stack(self, cnxt, stack_identity, template, params,
|
||||||
|
files, args):
|
||||||
|
"""
|
||||||
|
The preview_update_stack method shows the resources that would be
|
||||||
|
changed with an update to an existing stack based on the provided
|
||||||
|
template and parameters. See update_stack for description of
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
This method *cannot* guarantee that an update will have the actions
|
||||||
|
specified because resource plugins can influence changes/replacements
|
||||||
|
at runtime.
|
||||||
|
|
||||||
|
Note that at this stage the template has already been fetched from the
|
||||||
|
heat-api process if using a template-url.
|
||||||
|
"""
|
||||||
|
# Get the database representation of the existing stack
|
||||||
|
db_stack = self._get_stack(cnxt, stack_identity)
|
||||||
|
LOG.info(_LI('Previewing update of stack %s'), db_stack.name)
|
||||||
|
|
||||||
|
current_stack = parser.Stack.load(cnxt, stack=db_stack)
|
||||||
|
|
||||||
|
# Now parse the template and any parameters for the updated
|
||||||
|
# stack definition.
|
||||||
|
env = environment.Environment(params)
|
||||||
|
if args.get(rpc_api.PARAM_EXISTING, None):
|
||||||
|
env.patch_previous_parameters(
|
||||||
|
current_stack.env,
|
||||||
|
args.get(rpc_api.PARAM_CLEAR_PARAMETERS, []))
|
||||||
|
tmpl = templatem.Template(template, files=files, env=env)
|
||||||
|
|
||||||
|
current_stack, updated_stack = self._prepare_stack_updates(
|
||||||
|
cnxt, current_stack, tmpl, params, files, args)
|
||||||
|
|
||||||
|
update_task = update.StackUpdate(current_stack, updated_stack, None)
|
||||||
|
|
||||||
|
actions = update_task.preview()
|
||||||
|
|
||||||
|
fmt_updated_res = lambda k: api.format_stack_resource(
|
||||||
|
updated_stack.resources.get(k))
|
||||||
|
fmt_current_res = lambda k: api.format_stack_resource(
|
||||||
|
current_stack.resources.get(k))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'unchanged': map(fmt_updated_res, actions['unchanged']),
|
||||||
|
'updated': map(fmt_current_res, actions['updated']),
|
||||||
|
'replaced': map(fmt_updated_res, actions['replaced']),
|
||||||
|
'added': map(fmt_updated_res, actions['added']),
|
||||||
|
'deleted': map(fmt_current_res, actions['deleted']),
|
||||||
|
}
|
||||||
|
|
||||||
@context.request_context
|
@context.request_context
|
||||||
def stack_cancel_update(self, cnxt, stack_identity,
|
def stack_cancel_update(self, cnxt, stack_identity,
|
||||||
cancel_with_rollback=True):
|
cancel_with_rollback=True):
|
||||||
|
@ -219,3 +219,43 @@ class StackUpdate(object):
|
|||||||
yield (res, self.new_stack[name])
|
yield (res, self.new_stack[name])
|
||||||
|
|
||||||
return dependencies.Dependencies(edges())
|
return dependencies.Dependencies(edges())
|
||||||
|
|
||||||
|
def preview(self):
|
||||||
|
upd_keys = set(self.new_stack.resources.keys())
|
||||||
|
cur_keys = set(self.existing_stack.resources.keys())
|
||||||
|
|
||||||
|
common_keys = cur_keys.intersection(upd_keys)
|
||||||
|
deleted_keys = cur_keys.difference(upd_keys)
|
||||||
|
added_keys = upd_keys.difference(cur_keys)
|
||||||
|
|
||||||
|
updated_keys = []
|
||||||
|
replaced_keys = []
|
||||||
|
|
||||||
|
for key in common_keys:
|
||||||
|
current_res = self.existing_stack.resources[key]
|
||||||
|
updated_res = self.new_stack.resources[key]
|
||||||
|
|
||||||
|
current_props = current_res.frozen_definition().properties(
|
||||||
|
current_res.properties_schema, current_res.context)
|
||||||
|
updated_props = updated_res.frozen_definition().properties(
|
||||||
|
updated_res.properties_schema, updated_res.context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if current_res._needs_update(updated_res.frozen_definition(),
|
||||||
|
current_res.frozen_definition(),
|
||||||
|
updated_props, current_props,
|
||||||
|
None, check_init_complete=False):
|
||||||
|
current_res.update_template_diff_properties(updated_props,
|
||||||
|
current_props)
|
||||||
|
updated_keys.append(key)
|
||||||
|
except resource.UpdateReplace:
|
||||||
|
replaced_keys.append(key)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'unchanged': list(set(common_keys).difference(
|
||||||
|
set(updated_keys + replaced_keys))),
|
||||||
|
'updated': updated_keys,
|
||||||
|
'replaced': replaced_keys,
|
||||||
|
'added': added_keys,
|
||||||
|
'deleted': deleted_keys,
|
||||||
|
}
|
||||||
|
@ -35,6 +35,7 @@ class EngineClient(object):
|
|||||||
1.12 - Add with_detail option for stack resources list
|
1.12 - Add with_detail option for stack resources list
|
||||||
1.13 - Add support for template functions list
|
1.13 - Add support for template functions list
|
||||||
1.14 - Add cancel_with_rollback option to stack_cancel_update
|
1.14 - Add cancel_with_rollback option to stack_cancel_update
|
||||||
|
1.15 - Add preview_update_stack() call
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BASE_RPC_API_VERSION = '1.0'
|
BASE_RPC_API_VERSION = '1.0'
|
||||||
@ -264,6 +265,32 @@ class EngineClient(object):
|
|||||||
files=files,
|
files=files,
|
||||||
args=args))
|
args=args))
|
||||||
|
|
||||||
|
def preview_update_stack(self, ctxt, stack_identity, template, params,
|
||||||
|
files, args):
|
||||||
|
"""
|
||||||
|
The preview_update_stack method returns the resources that would be
|
||||||
|
changed in an update of an existing stack based on the provided
|
||||||
|
template and parameters.
|
||||||
|
|
||||||
|
Requires RPC version 1.15 or above.
|
||||||
|
|
||||||
|
:param ctxt: RPC context.
|
||||||
|
:param stack_identity: Name of the stack you wish to update.
|
||||||
|
:param template: New template for the stack.
|
||||||
|
:param params: Stack Input Params/Environment
|
||||||
|
:param files: files referenced from the environment.
|
||||||
|
:param args: Request parameters/args passed from API
|
||||||
|
"""
|
||||||
|
return self.call(ctxt,
|
||||||
|
self.make_msg('preview_update_stack',
|
||||||
|
stack_identity=stack_identity,
|
||||||
|
template=template,
|
||||||
|
params=params,
|
||||||
|
files=files,
|
||||||
|
args=args,
|
||||||
|
),
|
||||||
|
version='1.15')
|
||||||
|
|
||||||
def validate_template(self, ctxt, template, params=None):
|
def validate_template(self, ctxt, template, params=None):
|
||||||
"""
|
"""
|
||||||
The validate_template method uses the stack parser to check
|
The validate_template method uses the stack parser to check
|
||||||
|
@ -1101,6 +1101,47 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
|
|||||||
|
|
||||||
self.assertEqual({'stack': 'formatted_stack'}, result)
|
self.assertEqual({'stack': 'formatted_stack'}, result)
|
||||||
|
|
||||||
|
def test_preview_update_stack(self, mock_enforce):
|
||||||
|
self._mock_enforce_setup(mock_enforce, 'preview_update', True)
|
||||||
|
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
|
||||||
|
template = {u'Foo': u'bar'}
|
||||||
|
parameters = {u'InstanceType': u'm1.xlarge'}
|
||||||
|
body = {'template': template,
|
||||||
|
'parameters': parameters,
|
||||||
|
'files': {},
|
||||||
|
'timeout_mins': 30}
|
||||||
|
|
||||||
|
req = self._put('/stacks/%(stack_name)s/%(stack_id)s/preview' %
|
||||||
|
identity, json.dumps(body))
|
||||||
|
resource_changes = {'updated': [],
|
||||||
|
'deleted': [],
|
||||||
|
'unchanged': [],
|
||||||
|
'added': [],
|
||||||
|
'replaced': []}
|
||||||
|
|
||||||
|
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
|
||||||
|
rpc_client.EngineClient.call(
|
||||||
|
req.context,
|
||||||
|
('preview_update_stack',
|
||||||
|
{'stack_identity': dict(identity),
|
||||||
|
'template': template,
|
||||||
|
'params': {'parameters': parameters,
|
||||||
|
'encrypted_param_names': [],
|
||||||
|
'parameter_defaults': {},
|
||||||
|
'resource_registry': {}},
|
||||||
|
'files': {},
|
||||||
|
'args': {'timeout_mins': 30}}),
|
||||||
|
version='1.15'
|
||||||
|
).AndReturn(resource_changes)
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
result = self.controller.preview_update(req, tenant_id=identity.tenant,
|
||||||
|
stack_name=identity.stack_name,
|
||||||
|
stack_id=identity.stack_id,
|
||||||
|
body=body)
|
||||||
|
self.assertEqual({'resource_changes': resource_changes}, result)
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
def test_lookup(self, mock_enforce):
|
def test_lookup(self, mock_enforce):
|
||||||
self._mock_enforce_setup(mock_enforce, 'lookup', True)
|
self._mock_enforce_setup(mock_enforce, 'lookup', True)
|
||||||
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '1')
|
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '1')
|
||||||
|
@ -39,7 +39,7 @@ class ServiceEngineTest(common.HeatTestCase):
|
|||||||
|
|
||||||
def test_make_sure_rpc_version(self):
|
def test_make_sure_rpc_version(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'1.14',
|
'1.15',
|
||||||
service.EngineService.RPC_API_VERSION,
|
service.EngineService.RPC_API_VERSION,
|
||||||
('RPC version is changed, please update this test to new version '
|
('RPC version is changed, please update this test to new version '
|
||||||
'and make sure additional test cases are added for RPC APIs '
|
'and make sure additional test cases are added for RPC APIs '
|
||||||
|
@ -894,6 +894,209 @@ class StackServiceAdoptUpdateTest(common.HeatTestCase):
|
|||||||
|
|
||||||
self.m.VerifyAll()
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def _test_stack_update_preview(self, orig_template, new_template):
|
||||||
|
stack_name = 'service_update_test_stack'
|
||||||
|
params = {'foo': 'bar'}
|
||||||
|
old_stack = tools.get_stack(stack_name, self.ctx,
|
||||||
|
template=orig_template)
|
||||||
|
sid = old_stack.store()
|
||||||
|
old_stack.set_stack_user_project_id('1234')
|
||||||
|
s = stack_object.Stack.get_by_id(self.ctx, sid)
|
||||||
|
|
||||||
|
stack = tools.get_stack(stack_name, self.ctx, template=new_template)
|
||||||
|
|
||||||
|
self._stub_update_mocks(s, old_stack)
|
||||||
|
|
||||||
|
templatem.Template(new_template, files=None,
|
||||||
|
env=stack.env).AndReturn(stack.t)
|
||||||
|
environment.Environment(params).AndReturn(stack.env)
|
||||||
|
parser.Stack(self.ctx, stack.name,
|
||||||
|
stack.t,
|
||||||
|
convergence=False,
|
||||||
|
current_traversal=None,
|
||||||
|
prev_raw_template_id=None,
|
||||||
|
current_deps=None,
|
||||||
|
disable_rollback=True,
|
||||||
|
nested_depth=0,
|
||||||
|
owner_id=None,
|
||||||
|
parent_resource=None,
|
||||||
|
stack_user_project_id='1234',
|
||||||
|
strict_validate=True,
|
||||||
|
tenant_id='test_tenant_id',
|
||||||
|
timeout_mins=60,
|
||||||
|
user_creds_id=u'1',
|
||||||
|
username='test_username').AndReturn(stack)
|
||||||
|
|
||||||
|
self.m.StubOutWithMock(stack, 'validate')
|
||||||
|
stack.validate().AndReturn(None)
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
api_args = {'timeout_mins': 60}
|
||||||
|
result = self.man.preview_update_stack(self.ctx,
|
||||||
|
old_stack.identifier(),
|
||||||
|
new_template, params, None,
|
||||||
|
api_args)
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test_stack_update_preview_added_unchanged(self):
|
||||||
|
orig_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
new_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
password:
|
||||||
|
type: OS::Heat::RandomString
|
||||||
|
properties:
|
||||||
|
length: 8
|
||||||
|
'''
|
||||||
|
|
||||||
|
result = self._test_stack_update_preview(orig_template, new_template)
|
||||||
|
|
||||||
|
added = [x for x in result['added']][0]
|
||||||
|
self.assertEqual(added['resource_name'], 'password')
|
||||||
|
unchanged = [x for x in result['unchanged']][0]
|
||||||
|
self.assertEqual(unchanged['resource_name'], 'web_server')
|
||||||
|
|
||||||
|
empty_sections = ('deleted', 'replaced', 'updated')
|
||||||
|
for section in empty_sections:
|
||||||
|
section_contents = [x for x in result[section]]
|
||||||
|
self.assertEqual(section_contents, [])
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_stack_update_preview_replaced(self):
|
||||||
|
orig_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
new_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test2
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
result = self._test_stack_update_preview(orig_template, new_template)
|
||||||
|
|
||||||
|
replaced = [x for x in result['replaced']][0]
|
||||||
|
self.assertEqual(replaced['resource_name'], 'web_server')
|
||||||
|
empty_sections = ('added', 'deleted', 'unchanged', 'updated')
|
||||||
|
for section in empty_sections:
|
||||||
|
section_contents = [x for x in result[section]]
|
||||||
|
self.assertEqual(section_contents, [])
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_stack_update_preview_updated(self):
|
||||||
|
orig_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
new_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.small
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
result = self._test_stack_update_preview(orig_template, new_template)
|
||||||
|
|
||||||
|
updated = [x for x in result['updated']][0]
|
||||||
|
self.assertEqual(updated['resource_name'], 'web_server')
|
||||||
|
empty_sections = ('added', 'deleted', 'unchanged', 'replaced')
|
||||||
|
for section in empty_sections:
|
||||||
|
section_contents = [x for x in result[section]]
|
||||||
|
self.assertEqual(section_contents, [])
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_stack_update_preview_deleted(self):
|
||||||
|
orig_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
password:
|
||||||
|
type: OS::Heat::RandomString
|
||||||
|
properties:
|
||||||
|
length: 8
|
||||||
|
'''
|
||||||
|
|
||||||
|
new_template = '''
|
||||||
|
heat_template_version: 2014-10-16
|
||||||
|
resources:
|
||||||
|
web_server:
|
||||||
|
type: OS::Nova::Server
|
||||||
|
properties:
|
||||||
|
image: F17-x86_64-gold
|
||||||
|
flavor: m1.large
|
||||||
|
key_name: test
|
||||||
|
user_data: wordpress
|
||||||
|
'''
|
||||||
|
|
||||||
|
result = self._test_stack_update_preview(orig_template, new_template)
|
||||||
|
|
||||||
|
deleted = [x for x in result['deleted']][0]
|
||||||
|
self.assertEqual(deleted['resource_name'], 'password')
|
||||||
|
unchanged = [x for x in result['unchanged']][0]
|
||||||
|
self.assertEqual(unchanged['resource_name'], 'web_server')
|
||||||
|
empty_sections = ('added', 'updated', 'replaced')
|
||||||
|
for section in empty_sections:
|
||||||
|
section_contents = [x for x in result[section]]
|
||||||
|
self.assertEqual(section_contents, [])
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
|
||||||
class StackConvergenceServiceCreateUpdateTest(common.HeatTestCase):
|
class StackConvergenceServiceCreateUpdateTest(common.HeatTestCase):
|
||||||
|
|
||||||
|
@ -175,6 +175,14 @@ class EngineRpcAPITestCase(common.HeatTestCase):
|
|||||||
files={},
|
files={},
|
||||||
args=mock.ANY)
|
args=mock.ANY)
|
||||||
|
|
||||||
|
def test_preview_update_stack(self):
|
||||||
|
self._test_engine_api('preview_update_stack', 'call',
|
||||||
|
stack_identity=self.identity,
|
||||||
|
template={u'Foo': u'bar'},
|
||||||
|
params={u'InstanceType': u'm1.xlarge'},
|
||||||
|
files={},
|
||||||
|
args=mock.ANY)
|
||||||
|
|
||||||
def test_get_template(self):
|
def test_get_template(self):
|
||||||
self._test_engine_api('get_template', 'call',
|
self._test_engine_api('get_template', 'call',
|
||||||
stack_identity=self.identity)
|
stack_identity=self.identity)
|
||||||
|
Loading…
Reference in New Issue
Block a user