OpenstackClient plugin for stack create
This change implements the "openstack stack create" command. Blueprint: heat-support-python-openstackclient Change-Id: I32289f53d482a817f7449724cfbabc2c154b1ffd
This commit is contained in:
parent
ec59701df0
commit
b9c176efa7
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
||||
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':
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
heat_template_version: 2013-05-23
|
|
@ -0,0 +1,7 @@
|
|||
heat_template_version: 2013-05-23
|
||||
|
||||
parameters:
|
||||
p1:
|
||||
type: string
|
||||
p2:
|
||||
type: number
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue