Pass flag to engine service to patch parameters
For stack-update, add a new PATCH ReST API and a new CLI argument named existing-parameters to indicate that the set of parameters should be patched over the existing parameter from the DB. A new method in environment handles the patching. Partially-implements: blueprint troubleshooting-low-level-control Partial-Bug: 1224828 Change-Id: I802e0dca44926be3a3f45fcaa995c866a4abf998
This commit is contained in:
parent
9b700a1b6e
commit
b98c19fbbf
|
@ -50,6 +50,7 @@
|
|||
"stacks:show": "rule:deny_stack_user",
|
||||
"stacks:template": "rule:deny_stack_user",
|
||||
"stacks:update": "rule:deny_stack_user",
|
||||
"stacks:update_patch": "rule:deny_stack_user",
|
||||
"stacks:validate_template": "rule:deny_stack_user",
|
||||
"stacks:snapshot": "rule:deny_stack_user",
|
||||
"stacks:show_snapshot": "rule:deny_stack_user",
|
||||
|
|
|
@ -106,6 +106,10 @@ class API(wsgi.Router):
|
|||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="update",
|
||||
conditions={'method': 'PUT'})
|
||||
stack_mapper.connect("stack_update_patch",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="update_patch",
|
||||
conditions={'method': 'PATCH'})
|
||||
stack_mapper.connect("stack_delete",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="delete",
|
||||
|
|
|
@ -56,9 +56,15 @@ class InstantiationData(object):
|
|||
'files',
|
||||
)
|
||||
|
||||
def __init__(self, data):
|
||||
"""Initialise from the request object."""
|
||||
def __init__(self, data, patch=False):
|
||||
"""
|
||||
Initialise from the request object.
|
||||
If called from the PATCH api, insert a flag for the engine code
|
||||
to distinguish.
|
||||
"""
|
||||
self.data = data
|
||||
if patch:
|
||||
self.data[engine_api.PARAM_EXISTING] = True
|
||||
|
||||
@staticmethod
|
||||
def format_parse(data, data_type):
|
||||
|
@ -343,6 +349,22 @@ class StackController(object):
|
|||
|
||||
raise exc.HTTPAccepted()
|
||||
|
||||
@util.identified_stack
|
||||
def update_patch(self, req, identity, body):
|
||||
"""
|
||||
Update an existing stack with a new template by patching the parameters
|
||||
Add the flag patch to the args so the engine code can distinguish
|
||||
"""
|
||||
data = InstantiationData(body, patch=True)
|
||||
self.rpc_client.update_stack(req.context,
|
||||
identity,
|
||||
data.template(),
|
||||
data.environment(),
|
||||
data.files(),
|
||||
data.args())
|
||||
|
||||
raise exc.HTTPAccepted()
|
||||
|
||||
@util.identified_stack
|
||||
def delete(self, req, identity):
|
||||
"""
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import itertools
|
||||
import os.path
|
||||
|
@ -366,6 +367,15 @@ class Environment(object):
|
|||
self.registry.load(env_snippet.get(env_fmt.RESOURCE_REGISTRY, {}))
|
||||
self.params.update(env_snippet.get(env_fmt.PARAMETERS, {}))
|
||||
|
||||
def patch_previous_parameters(self, previous_env):
|
||||
"""This instance of Environment is the new environment where
|
||||
we are reusing as default the previous parameter values.
|
||||
"""
|
||||
previous_parameters = copy.deepcopy(previous_env.params)
|
||||
# patch the new set of parameters
|
||||
previous_parameters.update(self.params)
|
||||
self.params = previous_parameters
|
||||
|
||||
def user_env_as_dict(self):
|
||||
"""Get the environment as a dict, ready for storing in the db."""
|
||||
return {env_fmt.RESOURCE_REGISTRY: self.registry.as_dict(),
|
||||
|
|
|
@ -657,6 +657,8 @@ class EngineService(service.Service):
|
|||
common_params.setdefault(rpc_api.PARAM_DISABLE_ROLLBACK,
|
||||
current_stack.disable_rollback)
|
||||
env = environment.Environment(params)
|
||||
if args.get(rpc_api.PARAM_EXISTING, None):
|
||||
env.patch_previous_parameters(current_stack.env)
|
||||
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
||||
env, **common_params)
|
||||
updated_stack.parameters.set_stack_id(current_stack.identifier())
|
||||
|
|
|
@ -15,10 +15,10 @@ ENGINE_TOPIC = 'engine'
|
|||
|
||||
PARAM_KEYS = (
|
||||
PARAM_TIMEOUT, PARAM_DISABLE_ROLLBACK, PARAM_ADOPT_STACK_DATA,
|
||||
PARAM_SHOW_DELETED, PARAM_SHOW_NESTED
|
||||
PARAM_SHOW_DELETED, PARAM_SHOW_NESTED, PARAM_EXISTING
|
||||
) = (
|
||||
'timeout_mins', 'disable_rollback', 'adopt_stack_data',
|
||||
'show_deleted', 'show_nested'
|
||||
'show_deleted', 'show_nested', 'existing'
|
||||
)
|
||||
|
||||
STACK_KEYS = (
|
||||
|
|
|
@ -284,6 +284,9 @@ class ControllerTest(object):
|
|||
def _put(self, path, data, content_type='application/json'):
|
||||
return self._data_request(path, data, content_type, method='PUT')
|
||||
|
||||
def _patch(self, path, data, content_type='application/json'):
|
||||
return self._data_request(path, data, content_type, method='PATCH')
|
||||
|
||||
def _url(self, id):
|
||||
host = 'server.test:8004'
|
||||
path = '/v1/%(tenant)s/stacks/%(stack_name)s/%(stack_id)s%(path)s' % id
|
||||
|
@ -1376,6 +1379,152 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
|||
self.assertEqual(403, resp.status_int)
|
||||
self.assertIn('403 Forbidden', six.text_type(resp))
|
||||
|
||||
def test_update_with_existing_parameters(self, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'update_patch', True)
|
||||
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
|
||||
template = {u'Foo': u'bar'}
|
||||
body = {'template': template,
|
||||
'parameters': {},
|
||||
'files': {},
|
||||
'timeout_mins': 30}
|
||||
|
||||
req = self._patch('/stacks/%(stack_name)s/%(stack_id)s' % identity,
|
||||
json.dumps(body))
|
||||
|
||||
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
|
||||
rpc_client.EngineClient.call(
|
||||
req.context,
|
||||
('update_stack',
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
'args': {rpc_api.PARAM_EXISTING: True,
|
||||
'timeout_mins': 30}})
|
||||
).AndReturn(dict(identity))
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.assertRaises(webob.exc.HTTPAccepted,
|
||||
self.controller.update_patch,
|
||||
req, tenant_id=identity.tenant,
|
||||
stack_name=identity.stack_name,
|
||||
stack_id=identity.stack_id,
|
||||
body=body)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_update_with_patched_existing_parameters(self, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'update_patch', 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._patch('/stacks/%(stack_name)s/%(stack_id)s' % identity,
|
||||
json.dumps(body))
|
||||
|
||||
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
|
||||
rpc_client.EngineClient.call(
|
||||
req.context,
|
||||
('update_stack',
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
'args': {rpc_api.PARAM_EXISTING: True,
|
||||
'timeout_mins': 30}})
|
||||
).AndReturn(dict(identity))
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.assertRaises(webob.exc.HTTPAccepted,
|
||||
self.controller.update_patch,
|
||||
req, tenant_id=identity.tenant,
|
||||
stack_name=identity.stack_name,
|
||||
stack_id=identity.stack_id,
|
||||
body=body)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_update_with_existing_and_default_parameters(
|
||||
self, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'update_patch', True)
|
||||
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
|
||||
template = {u'Foo': u'bar'}
|
||||
clear_params = [u'DBUsername', u'DBPassword', u'LinuxDistribution']
|
||||
body = {'template': template,
|
||||
'parameters': {},
|
||||
'clear_parameters': clear_params,
|
||||
'files': {},
|
||||
'timeout_mins': 30}
|
||||
|
||||
req = self._patch('/stacks/%(stack_name)s/%(stack_id)s' % identity,
|
||||
json.dumps(body))
|
||||
|
||||
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
|
||||
rpc_client.EngineClient.call(
|
||||
req.context,
|
||||
('update_stack',
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': {},
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
'args': {rpc_api.PARAM_EXISTING: True,
|
||||
'clear_parameters': clear_params,
|
||||
'timeout_mins': 30}})
|
||||
).AndReturn(dict(identity))
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.assertRaises(webob.exc.HTTPAccepted,
|
||||
self.controller.update_patch,
|
||||
req, tenant_id=identity.tenant,
|
||||
stack_name=identity.stack_name,
|
||||
stack_id=identity.stack_id,
|
||||
body=body)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_update_with_patched_and_default_parameters(
|
||||
self, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'update_patch', True)
|
||||
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
|
||||
template = {u'Foo': u'bar'}
|
||||
parameters = {u'InstanceType': u'm1.xlarge'}
|
||||
clear_params = [u'DBUsername', u'DBPassword', u'LinuxDistribution']
|
||||
body = {'template': template,
|
||||
'parameters': parameters,
|
||||
'clear_parameters': clear_params,
|
||||
'files': {},
|
||||
'timeout_mins': 30}
|
||||
|
||||
req = self._patch('/stacks/%(stack_name)s/%(stack_id)s' % identity,
|
||||
json.dumps(body))
|
||||
|
||||
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
|
||||
rpc_client.EngineClient.call(
|
||||
req.context,
|
||||
('update_stack',
|
||||
{'stack_identity': dict(identity),
|
||||
'template': template,
|
||||
'params': {'parameters': parameters,
|
||||
'resource_registry': {}},
|
||||
'files': {},
|
||||
'args': {rpc_api.PARAM_EXISTING: True,
|
||||
'clear_parameters': clear_params,
|
||||
'timeout_mins': 30}})
|
||||
).AndReturn(dict(identity))
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.assertRaises(webob.exc.HTTPAccepted,
|
||||
self.controller.update_patch,
|
||||
req, tenant_id=identity.tenant,
|
||||
stack_name=identity.stack_name,
|
||||
stack_id=identity.stack_id,
|
||||
body=body)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_delete(self, mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'delete', True)
|
||||
identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
|
||||
|
|
|
@ -81,6 +81,30 @@ wp_template = '''
|
|||
}
|
||||
'''
|
||||
|
||||
wp_template_no_default = '''
|
||||
{
|
||||
"AWSTemplateFormatVersion" : "2010-09-09",
|
||||
"Description" : "WordPress",
|
||||
"Parameters" : {
|
||||
"KeyName" : {
|
||||
"Description" : "KeyName",
|
||||
"Type" : "String"
|
||||
}
|
||||
},
|
||||
"Resources" : {
|
||||
"WebServer": {
|
||||
"Type": "AWS::EC2::Instance",
|
||||
"Properties": {
|
||||
"ImageId" : "F17-x86_64-gold",
|
||||
"InstanceType" : "m1.large",
|
||||
"KeyName" : "test",
|
||||
"UserData" : "wordpress"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
nested_alarm_template = '''
|
||||
HeatTemplateFormatVersion: '2012-12-12'
|
||||
Resources:
|
||||
|
@ -184,6 +208,14 @@ def get_wordpress_stack(stack_name, ctx):
|
|||
return stack
|
||||
|
||||
|
||||
def get_wordpress_stack_no_params(stack_name, ctx):
|
||||
t = template_format.parse(wp_template)
|
||||
template = parser.Template(t)
|
||||
stack = parser.Stack(ctx, stack_name, template,
|
||||
environment.Environment({}))
|
||||
return stack
|
||||
|
||||
|
||||
def get_stack(stack_name, ctx, template):
|
||||
t = template_format.parse(template)
|
||||
template = templatem.Template(t)
|
||||
|
@ -896,6 +928,56 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
|
|||
self.assertEqual(self.man.thread_group_mgr.events[sid], [evt_mock])
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_stack_update_existing_parameters(self):
|
||||
'''Use a template with default parameter and no input parameter
|
||||
then update with a template without default and no input
|
||||
parameter, using the existing parameter.
|
||||
'''
|
||||
stack_name = 'service_update_test_stack_existing_parameters'
|
||||
no_params = {}
|
||||
with_params = {'KeyName': 'foo'}
|
||||
|
||||
old_stack = get_wordpress_stack_no_params(stack_name, self.ctx)
|
||||
sid = old_stack.store()
|
||||
s = db_api.stack_get(self.ctx, sid)
|
||||
|
||||
t = template_format.parse(wp_template_no_default)
|
||||
template = parser.Template(t)
|
||||
env = environment.Environment({'parameters': with_params,
|
||||
'resource_registry': {'rsc': 'test'}})
|
||||
stack = parser.Stack(self.ctx, stack_name, template, env)
|
||||
|
||||
self._stub_update_mocks(s, old_stack)
|
||||
|
||||
templatem.Template(wp_template_no_default,
|
||||
files=None).AndReturn(stack.t)
|
||||
environment.Environment(no_params).AndReturn(old_stack.env)
|
||||
parser.Stack(self.ctx, stack.name,
|
||||
stack.t, old_stack.env,
|
||||
timeout_mins=60, disable_rollback=True).AndReturn(stack)
|
||||
|
||||
self.m.StubOutWithMock(stack, 'validate')
|
||||
stack.validate().AndReturn(None)
|
||||
|
||||
evt_mock = self.m.CreateMockAnything()
|
||||
self.m.StubOutWithMock(grevent, 'Event')
|
||||
grevent.Event().AndReturn(evt_mock)
|
||||
self.m.StubOutWithMock(threadgroup, 'ThreadGroup')
|
||||
threadgroup.ThreadGroup().AndReturn(DummyThreadGroup())
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
api_args = {engine_api.PARAM_TIMEOUT: 60,
|
||||
engine_api.PARAM_EXISTING: True}
|
||||
result = self.man.update_stack(self.ctx, old_stack.identifier(),
|
||||
wp_template_no_default, no_params,
|
||||
None, api_args)
|
||||
self.assertEqual(old_stack.identifier(), result)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertTrue(result['stack_id'])
|
||||
self.assertEqual(self.man.thread_group_mgr.events[sid], [evt_mock])
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_stack_update_reuses_api_params(self):
|
||||
stack_name = 'service_update_test_stack'
|
||||
params = {'foo': 'bar'}
|
||||
|
|
|
@ -47,6 +47,32 @@ class EnvironmentTest(common.HeatTestCase):
|
|||
env = environment.Environment(new_env)
|
||||
self.assertEqual(new_env, env.user_env_as_dict())
|
||||
|
||||
def test_existing_parameters(self):
|
||||
# This tests reusing the existing parameters as is
|
||||
prev_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
params = {}
|
||||
expected = {'parameters': prev_params,
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
{'parameters': prev_params,
|
||||
'resource_registry': {'resources': {}}})
|
||||
env = environment.Environment(params)
|
||||
env.patch_previous_parameters(prev_env)
|
||||
self.assertEqual(expected, env.user_env_as_dict())
|
||||
|
||||
def test_patch_existing_parameters(self):
|
||||
# This tests patching cli parameters over the existing parameters
|
||||
prev_params = {'foo': 'bar', 'tester': 'Yes'}
|
||||
params = {'tester': 'patched'}
|
||||
expected = {'parameters': {'foo': 'bar', 'tester': 'patched'},
|
||||
'resource_registry': {'resources': {}}}
|
||||
prev_env = environment.Environment(
|
||||
{'parameters': prev_params,
|
||||
'resource_registry': {'resources': {}}})
|
||||
env = environment.Environment(params)
|
||||
env.patch_previous_parameters(prev_env)
|
||||
self.assertEqual(expected, env.user_env_as_dict())
|
||||
|
||||
def test_global_registry(self):
|
||||
self.g_env.register_class('CloudX::Nova::Server',
|
||||
generic_resource.GenericResource)
|
||||
|
|
Loading…
Reference in New Issue