summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--heatclient/common/template_utils.py8
-rw-r--r--heatclient/osc/v1/stack.py164
-rw-r--r--heatclient/tests/unit/osc/v1/test_stack.py174
-rw-r--r--setup.cfg1
4 files changed, 344 insertions, 3 deletions
diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py
index d6e366f..aa4583c 100644
--- a/heatclient/common/template_utils.py
+++ b/heatclient/common/template_utils.py
@@ -27,17 +27,19 @@ from heatclient import exc
from heatclient.openstack.common._i18n import _
-def process_template_path(template_path, object_request=None):
+def process_template_path(template_path, object_request=None, existing=False):
"""Read template from template path.
Attempt to read template first as a file or url. If that is unsuccessful,
try again to assuming path is to a template object.
"""
try:
- return get_template_contents(template_file=template_path)
+ return get_template_contents(template_file=template_path,
+ existing=existing)
except error.URLError:
return get_template_contents(template_object=template_path,
- object_request=object_request)
+ object_request=object_request,
+ existing=existing)
def get_template_contents(template_file=None, template_url=None,
diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py
index 34f7ccb..d7f905c 100644
--- a/heatclient/osc/v1/stack.py
+++ b/heatclient/osc/v1/stack.py
@@ -163,6 +163,170 @@ class CreateStack(show.ShowOne):
return _show_stack(client, stack['id'], format='table', short=True)
+class UpdateStack(show.ShowOne):
+ """Update a stack."""
+
+ log = logging.getLogger(__name__ + '.UpdateStack')
+
+ def get_parser(self, prog_name):
+ parser = super(UpdateStack, self).get_parser(prog_name)
+ parser.add_argument(
+ '-t', '--template', metavar='<FILE or URL>',
+ help=_('Path to the template')
+ )
+ parser.add_argument(
+ '-e', '--environment', metavar='<FILE or URL>',
+ action='append',
+ help=_('Path to the environment. Can be specified multiple times')
+ )
+ parser.add_argument(
+ '--pre-update', metavar='<RESOURCE>', action='append',
+ help=_('Name of a resource to set a pre-update hook to. Resources '
+ 'in nested stacks can be set using slash as a separator: '
+ 'nested_stack/another/my_resource. You can use wildcards '
+ 'to match multiple stacks or resources: '
+ 'nested_stack/an*/*_resource. This can be specified '
+ 'multiple times')
+ )
+ parser.add_argument(
+ '--timeout', metavar='<TIMEOUT>', type=int,
+ help=_('Stack update timeout in minutes')
+ )
+ parser.add_argument(
+ '--rollback', metavar='<VALUE>',
+ help=_('Set rollback on update failure. '
+ 'Value "enabled" sets rollback to enabled. '
+ 'Value "disabled" sets rollback to disabled. '
+ 'Value "keep" uses the value of existing stack to be '
+ 'updated (default)')
+ )
+ parser.add_argument(
+ '--dry-run', action="store_true",
+ help=_('Do not actually perform the stack update, but show what '
+ 'would be changed')
+ )
+ parser.add_argument(
+ '--parameter', metavar='<KEY=VALUE>',
+ help=_('Parameter values used to create the stack. '
+ 'This can be specified multiple times'),
+ action='append'
+ )
+ parser.add_argument(
+ '--parameter-file', metavar='<KEY=FILE>',
+ help=_('Parameter values from file used to create the stack. '
+ 'This can be specified multiple times. Parameter value '
+ 'would be the content of the file'),
+ action='append'
+ )
+ parser.add_argument(
+ '--existing', action="store_true",
+ help=_('Re-use the template, parameters and environment of the '
+ 'current stack. If the template argument is omitted then '
+ 'the existing template is used. If no %(env_arg)s is '
+ 'specified then the existing environment is used. '
+ 'Parameters specified in %(arg)s will patch over the '
+ 'existing values in the current stack. Parameters omitted '
+ 'will keep the existing values') % {
+ 'arg': '--parameter', 'env_arg': '--environment'}
+ )
+ parser.add_argument(
+ '--clear-parameter', metavar='<PARAMETER>',
+ help=_('Remove the parameters from the set of parameters of '
+ 'current stack for the %(cmd)s. The default value in the '
+ 'template will be used. This can be specified multiple '
+ 'times') % {'cmd': 'stack-update'},
+ action='append'
+ )
+ parser.add_argument(
+ 'stack', metavar='<STACK>',
+ help=_('Name or ID of stack to update')
+ )
+ parser.add_argument(
+ '--tags', metavar='<TAG1,TAG2>',
+ help=_('An updated list of tags to associate with the stack')
+ )
+ parser.add_argument(
+ '--wait',
+ action='store_true',
+ help=_('Wait until stack goes to UPDATE_COMPLETE or '
+ 'UPDATE_FAILED')
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug('take_action(%s)', parsed_args)
+
+ client = self.app.client_manager.orchestration
+
+ tpl_files, template = template_utils.process_template_path(
+ parsed_args.template,
+ object_request=_authenticated_fetcher(client),
+ existing=parsed_args.existing)
+
+ env_files, env = (
+ template_utils.process_multiple_environments_and_files(
+ env_paths=parsed_args.environment))
+
+ parameters = heat_utils.format_all_parameters(
+ parsed_args.parameter,
+ parsed_args.parameter_file,
+ parsed_args.template)
+
+ if parsed_args.pre_update:
+ template_utils.hooks_to_env(env, parsed_args.pre_update,
+ 'pre-update')
+
+ fields = {
+ 'stack_id': parsed_args.stack,
+ 'parameters': parameters,
+ 'existing': parsed_args.existing,
+ 'template': template,
+ 'files': dict(list(tpl_files.items()) + list(env_files.items())),
+ 'environment': env
+ }
+
+ if parsed_args.tags:
+ fields['tags'] = parsed_args.tags
+ if parsed_args.timeout:
+ fields['timeout_mins'] = parsed_args.timeout
+ if parsed_args.clear_parameter:
+ fields['clear_parameters'] = list(parsed_args.clear_parameter)
+
+ if parsed_args.rollback:
+ rollback = parsed_args.rollback.strip().lower()
+ if rollback not in ('enabled', 'disabled', 'keep'):
+ msg = _('--rollback invalid value: %s') % parsed_args.rollback
+ raise exc.CommandError(msg)
+ if rollback != 'keep':
+ fields['disable_rollback'] = rollback == 'disabled'
+
+ if parsed_args.dry_run:
+ changes = client.stacks.preview_update(**fields)
+
+ fields = ['state', 'resource_name', 'resource_type',
+ 'resource_identity']
+
+ columns = sorted(changes.get("resource_changes", {}).keys())
+ data = [heat_utils.json_formatter(changes["resource_changes"][key])
+ for key in columns]
+
+ return columns, data
+
+ client.stacks.update(**fields)
+ if parsed_args.wait:
+ if not utils.wait_for_status(client.stacks.get, parsed_args.stack,
+ status_field='stack_status',
+ success_status='update_complete',
+ error_status='update_failed'):
+
+ msg = _('Stack %s failed to update.') % parsed_args.stack
+ raise exc.CommandError(msg)
+
+ return _show_stack(client, parsed_args.stack, format='table',
+ short=True)
+
+
class ShowStack(show.ShowOne):
"""Show stack details"""
diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py
index bf2fa5f..cdec6cd 100644
--- a/heatclient/tests/unit/osc/v1/test_stack.py
+++ b/heatclient/tests/unit/osc/v1/test_stack.py
@@ -13,6 +13,7 @@
import copy
import mock
+import six
import testscenarios
from openstackclient.common import exceptions as exc
@@ -137,6 +138,179 @@ class TestStackCreate(TestStack):
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
+class TestStackUpdate(TestStack):
+
+ template_path = 'heatclient/tests/test_templates/empty.yaml'
+
+ defaults = {
+ 'stack_id': 'my_stack',
+ 'environment': {},
+ 'existing': False,
+ 'files': {},
+ 'template': {'heat_template_version': '2013-05-23'},
+ 'parameters': {},
+ }
+
+ def setUp(self):
+ super(TestStackUpdate, self).setUp()
+ self.cmd = stack.UpdateStack(self.app, None)
+ self.stack_client.update = mock.MagicMock(
+ return_value={'stack': {'id': '1234'}})
+ self.stack_client.preview_update = mock.MagicMock(
+ return_value={'resource_changes': {'added': [],
+ 'deleted': [],
+ 'replaced': [],
+ 'unchanged': [],
+ 'updated': []}})
+ self.stack_client.get = mock.MagicMock(
+ return_value={'stack_status': 'create_complete'})
+ stack._authenticated_fetcher = mock.MagicMock()
+
+ def test_stack_update_defaults(self):
+ arglist = ['my_stack', '-t', self.template_path]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**self.defaults)
+
+ def test_stack_update_rollback_enabled(self):
+ arglist = ['my_stack', '-t', self.template_path, '--rollback',
+ 'enabled']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['disable_rollback'] = False
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_rollback_disabled(self):
+ arglist = ['my_stack', '-t', self.template_path, '--rollback',
+ 'disabled']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['disable_rollback'] = True
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_rollback_keep(self):
+ arglist = ['my_stack', '-t', self.template_path, '--rollback',
+ 'keep']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.assertNotIn('disable_rollback', self.defaults)
+ self.stack_client.update.assert_called_with(**self.defaults)
+
+ def test_stack_update_rollback_invalid(self):
+ arglist = ['my_stack', '-t', self.template_path, '--rollback', 'foo']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['disable_rollback'] = False
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ ex = self.assertRaises(exc.CommandError, self.cmd.take_action,
+ parsed_args)
+ self.assertEqual("--rollback invalid value: foo", six.text_type(ex))
+
+ def test_stack_update_parameters(self):
+ template_path = ('/'.join(self.template_path.split('/')[:-1]) +
+ '/parameters.yaml')
+ arglist = ['my_stack', '-t', template_path, '--parameter', 'p1=a',
+ '--parameter', 'p2=6']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['parameters'] = {'p1': 'a', 'p2': '6'}
+ kwargs['template']['parameters'] = {'p1': {'type': 'string'},
+ 'p2': {'type': 'number'}}
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_clear_parameters(self):
+ arglist = ['my_stack', '-t', self.template_path, '--clear-parameter',
+ 'a']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['clear_parameters'] = ['a']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_tags(self):
+ arglist = ['my_stack', '-t', self.template_path, '--tags', 'tag1,tag2']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['tags'] = 'tag1,tag2'
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_timeout(self):
+ arglist = ['my_stack', '-t', self.template_path, '--timeout', '60']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['timeout_mins'] = 60
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_pre_update(self):
+ arglist = ['my_stack', '-t', self.template_path, '--pre-update', 'a']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['environment'] = {
+ 'resource_registry': {'resources': {'a': {'hooks': 'pre-update'}}}
+ }
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_existing(self):
+ arglist = ['my_stack', '-t', self.template_path, '--existing']
+ kwargs = copy.deepcopy(self.defaults)
+ kwargs['existing'] = True
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**kwargs)
+
+ def test_stack_update_dry_run(self):
+ arglist = ['my_stack', '-t', self.template_path, '--dry-run']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.preview_update.assert_called_with(**self.defaults)
+ self.stack_client.update.assert_not_called()
+
+ def test_stack_update_wait(self):
+ arglist = ['my_stack', '-t', self.template_path, '--wait']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.update.assert_called_with(**self.defaults)
+ self.stack_client.get.assert_called_with(**{'stack_id': 'my_stack'})
+
+ @mock.patch('openstackclient.common.utils.wait_for_status',
+ return_value=False)
+ def test_stack_update_wait_fail(self, mock_wait):
+ arglist = ['my_stack', '-t', self.template_path, '--wait']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
+
+
class TestStackShow(TestStack):
scenarios = [
diff --git a/setup.cfg b/setup.cfg
index f7ebbb3..d11fa7a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,6 +33,7 @@ openstack.orchestration.v1 =
stack_show = heatclient.osc.v1.stack:ShowStack
stack_list = heatclient.osc.v1.stack:ListStack
stack_create = heatclient.osc.v1.stack:CreateStack
+ stack_update = heatclient.osc.v1.stack:UpdateStack
[global]