summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--heatclient/common/template_utils.py38
-rw-r--r--heatclient/osc/v1/stack.py161
-rw-r--r--heatclient/tests/test_templates/empty.yaml1
-rw-r--r--heatclient/tests/test_templates/parameters.yaml7
-rw-r--r--heatclient/tests/unit/osc/v1/test_stack.py118
-rw-r--r--heatclient/v1/shell.py31
-rw-r--r--setup.cfg1
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,
diff --git a/setup.cfg b/setup.cfg
index be9a2a4..f7ebbb3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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]