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:
Ton Ngo 2014-07-23 09:55:40 -07:00
parent 9b700a1b6e
commit b98c19fbbf
9 changed files with 300 additions and 4 deletions

View File

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

View File

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

View File

@ -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):
"""

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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