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:
Bryan Jones 2015-11-03 19:14:07 +00:00
parent ec59701df0
commit b9c176efa7
7 changed files with 318 additions and 39 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
heat_template_version: 2013-05-23

View File

@ -0,0 +1,7 @@
heat_template_version: 2013-05-23
parameters:
p1:
type: string
p2:
type: number

View File

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

View File

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

View File

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