diff options
-rw-r--r-- | heatclient/common/template_utils.py | 38 | ||||
-rw-r--r-- | heatclient/osc/v1/stack.py | 161 | ||||
-rw-r--r-- | heatclient/tests/test_templates/empty.yaml | 1 | ||||
-rw-r--r-- | heatclient/tests/test_templates/parameters.yaml | 7 | ||||
-rw-r--r-- | heatclient/tests/unit/osc/v1/test_stack.py | 118 | ||||
-rw-r--r-- | heatclient/v1/shell.py | 31 | ||||
-rw-r--r-- | setup.cfg | 1 |
7 files changed, 318 insertions, 39 deletions
diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index 2134a76..d6e366f 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -16,6 +16,7 @@ import collections from oslo_serialization import jsonutils import six +from six.moves.urllib import error from six.moves.urllib import parse from six.moves.urllib import request @@ -26,6 +27,19 @@ from heatclient import exc from heatclient.openstack.common._i18n import _ +def process_template_path(template_path, object_request=None): + """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) + except error.URLError: + return get_template_contents(template_object=template_path, + object_request=object_request) + + def get_template_contents(template_file=None, template_url=None, template_object=None, object_request=None, files=None, existing=False): @@ -235,3 +249,27 @@ def resolve_environment_urls(resource_registry, files, env_base_url): res_base_url = res_dict.get('base_url', base_url) get_file_contents( res_dict, files, res_base_url, ignore_if) + + +def hooks_to_env(env, arg_hooks, hook): + """Add hooks from args to environment's resource_registry section. + + Hooks are either "resource_name" (if it's a top-level resource) or + "nested_stack/resource_name" (if the resource is in a nested stack). + + The environment expects each hook to be associated with the resource + within `resource_registry/resources` using the `hooks: pre-create` format. + """ + if 'resource_registry' not in env: + env['resource_registry'] = {} + if 'resources' not in env['resource_registry']: + env['resource_registry']['resources'] = {} + for hook_declaration in arg_hooks: + hook_path = [r for r in hook_declaration.split('/') if r] + resources = env['resource_registry']['resources'] + for nested_stack in hook_path: + if nested_stack not in resources: + resources[nested_stack] = {} + resources = resources[nested_stack] + else: + resources['hooks'] = hook diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 6e471f0..34f7ccb 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -21,11 +21,148 @@ from openstackclient.common import exceptions as exc from openstackclient.common import parseractions from openstackclient.common import utils +from heatclient.common import http +from heatclient.common import template_utils from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ +def _authenticated_fetcher(client): + def _do(*args, **kwargs): + if isinstance(client.http_client, http.SessionClient): + method, url = args + return client.http_client.request(url, method, **kwargs).content + else: + return client.http_client.raw_request(*args, **kwargs).content + + return _do + + +class CreateStack(show.ShowOne): + """Create a stack.""" + + log = logging.getLogger(__name__ + '.CreateStack') + + def get_parser(self, prog_name): + parser = super(CreateStack, self).get_parser(prog_name) + parser.add_argument( + '-t', '--template', + metavar='<FILE or URL>', + required=True, + 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( + '--timeout', + metavar='<TIMEOUT>', + type=int, + help=_('Stack creating timeout in minutes') + ) + parser.add_argument( + '--pre-create', + metavar='<RESOURCE>', + default=None, + action='append', + help=_('Name of a resource to set a pre-create 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( + '--enable-rollback', + action='store_true', + help=_('Enable rollback on create/update failure') + ) + parser.add_argument( + '--parameter', + metavar='<KEY=VALUE>', + action='append', + help=_('Parameter values used to create the stack. This can be ' + 'specified multiple times') + ) + parser.add_argument( + '--parameter-file', + metavar='<KEY=FILE>', + action='append', + help=_('Parameter values from file used to create the stack. ' + 'This can be specified multiple times. Parameter values ' + 'would be the content of the file') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait until stack completes') + ) + parser.add_argument( + '--tags', + metavar='<TAG1,TAG2...>', + help=_('A list of tags to associate with the stack') + ) + parser.add_argument( + 'name', + metavar='<STACK_NAME>', + help=_('Name of the stack to create') + ) + + 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)) + + 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_create: + template_utils.hooks_to_env(env, parsed_args.pre_create, + 'pre-create') + + fields = { + 'stack_name': parsed_args.name, + 'disable_rollback': not parsed_args.enable_rollback, + 'parameters': parameters, + '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 + + stack = client.stacks.create(**fields)['stack'] + if parsed_args.wait: + if not utils.wait_for_status(client.stacks.get, parsed_args.name, + status_field='stack_status', + success_status='create_complete', + error_status='create_failed'): + + msg = _('Stack %s failed to create.') % parsed_args.name + raise exc.CommandError(msg) + + return _show_stack(client, stack['id'], format='table', short=True) + + class ShowStack(show.ShowOne): """Show stack details""" @@ -48,7 +185,7 @@ class ShowStack(show.ShowOne): format=parsed_args.formatter) -def _show_stack(heat_client, stack_id, format): +def _show_stack(heat_client, stack_id, format='', short=False): try: data = heat_client.stacks.get(stack_id=stack_id) except heat_exc.HTTPNotFound: @@ -63,15 +200,21 @@ def _show_stack(heat_client, stack_id, format): 'updated_time', 'stack_status', 'stack_status_reason', - 'parameters', - 'outputs', - 'links', ] - exclude_columns = ('template_description',) - for key in data.to_dict(): - # add remaining columns without an explicit order - if key not in columns and key not in exclude_columns: - columns.append(key) + + if not short: + columns += [ + 'parameters', + 'outputs', + 'links', + ] + + exclude_columns = ('template_description',) + for key in data.to_dict(): + # add remaining columns without an explicit order + if key not in columns and key not in exclude_columns: + columns.append(key) + formatters = {} complex_formatter = None if format in 'table': diff --git a/heatclient/tests/test_templates/empty.yaml b/heatclient/tests/test_templates/empty.yaml new file mode 100644 index 0000000..d7e8090 --- /dev/null +++ b/heatclient/tests/test_templates/empty.yaml @@ -0,0 +1 @@ +heat_template_version: 2013-05-23 diff --git a/heatclient/tests/test_templates/parameters.yaml b/heatclient/tests/test_templates/parameters.yaml new file mode 100644 index 0000000..6be2321 --- /dev/null +++ b/heatclient/tests/test_templates/parameters.yaml @@ -0,0 +1,7 @@ +heat_template_version: 2013-05-23 + +parameters: + p1: + type: string + p2: + type: number diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 2f33ed1..bf2fa5f 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -32,6 +32,111 @@ class TestStack(orchestration_fakes.TestOrchestrationv1): self.stack_client = self.app.client_manager.orchestration.stacks +class TestStackCreate(TestStack): + + template_path = 'heatclient/tests/test_templates/empty.yaml' + + defaults = { + 'stack_name': 'my_stack', + 'disable_rollback': True, + 'parameters': {}, + 'template': {'heat_template_version': '2013-05-23'}, + 'files': {}, + 'environment': {} + } + + def setUp(self): + super(TestStackCreate, self).setUp() + self.cmd = stack.CreateStack(self.app, None) + self.stack_client.create = mock.MagicMock( + return_value={'stack': {'id': '1234'}}) + self.stack_client.get = mock.MagicMock( + return_value={'stack_status': 'create_complete'}) + stack._authenticated_fetcher = mock.MagicMock() + + def test_stack_create_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.create.assert_called_with(**self.defaults) + + def test_stack_create_rollback(self): + arglist = ['my_stack', '-t', self.template_path, '--enable-rollback'] + 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.create.assert_called_with(**kwargs) + + def test_stack_create_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.create.assert_called_with(**kwargs) + + def test_stack_create_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.create.assert_called_with(**kwargs) + + def test_stack_create_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.create.assert_called_with(**kwargs) + + def test_stack_create_pre_create(self): + arglist = ['my_stack', '-t', self.template_path, '--pre-create', 'a'] + kwargs = copy.deepcopy(self.defaults) + kwargs['environment'] = { + 'resource_registry': {'resources': {'a': {'hooks': 'pre-create'}}} + } + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_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.create.assert_called_with(**self.defaults) + self.stack_client.get.assert_called_with(**{'stack_id': '1234'}) + + @mock.patch('openstackclient.common.utils.wait_for_status', + return_value=False) + def test_stack_create_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 = [ @@ -75,17 +180,26 @@ class TestStackShow(TestStack): def setUp(self): super(TestStackShow, self).setUp() self.cmd = stack.ShowStack(self.app, None) + self.stack_client.get = mock.Mock( + return_value=stacks.Stack(None, self.get_response)) def test_stack_show(self): arglist = ['--format', self.format, 'my_stack'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.stack_client.get = mock.Mock( - return_value=stacks.Stack(None, self.get_response)) self.cmd.take_action(parsed_args) self.stack_client.get.assert_called_with(**{ 'stack_id': 'my_stack', }) + def test_stack_show_short(self): + expected = ['id', 'stack_name', 'description', 'creation_time', + 'updated_time', 'stack_status', 'stack_status_reason'] + + columns, data = stack._show_stack(self.mock_client, 'my_stack', + short=True) + + self.assertEqual(expected, columns) + class TestStackList(TestStack): diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index d857602..1fc6b4e 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -116,7 +116,7 @@ def do_stack_create(hc, args): 'arg2': '-t/--timeout'}) if args.pre_create: - hooks_to_env(env, args.pre_create, 'pre-create') + template_utils.hooks_to_env(env, args.pre_create, 'pre-create') fields = { 'stack_name': args.name, @@ -139,32 +139,7 @@ def do_stack_create(hc, args): hc.stacks.create(**fields) do_stack_list(hc) if args.poll is not None: - _poll_for_events(hc, args.name, 'CREATE', poll_period=args.poll) - - -def hooks_to_env(env, arg_hooks, hook): - '''Add hooks from args to environment's resource_registry section. - - Hooks are either "resource_name" (if it's a top-level resource) or - "nested_stack/resource_name" (if the resource is in a nested stack). - - The environment expects each hook to be associated with the resource - within `resource_registry/resources` using the `hooks: pre-create` format. - - ''' - if 'resource_registry' not in env: - env['resource_registry'] = {} - if 'resources' not in env['resource_registry']: - env['resource_registry']['resources'] = {} - for hook_declaration in arg_hooks: - hook_path = [r for r in hook_declaration.split('/') if r] - resources = env['resource_registry']['resources'] - for nested_stack in hook_path: - if nested_stack not in resources: - resources[nested_stack] = {} - resources = resources[nested_stack] - else: - resources['hooks'] = hook + _poll_for_events(hc, args.name, 'CREATE', args.poll) @utils.arg('-e', '--environment-file', metavar='<FILE or URL>', @@ -477,7 +452,7 @@ def do_stack_update(hc, args): env_paths=args.environment_file) if args.pre_update: - hooks_to_env(env, args.pre_update, 'pre-update') + template_utils.hooks_to_env(env, args.pre_update, 'pre-update') fields = { 'stack_id': args.id, @@ -32,6 +32,7 @@ openstack.cli.extension = openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack + stack_create = heatclient.osc.v1.stack:CreateStack [global] |