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:
@@ -16,6 +16,7 @@
|
|||||||
import collections
|
import collections
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
import six
|
import six
|
||||||
|
from six.moves.urllib import error
|
||||||
from six.moves.urllib import parse
|
from six.moves.urllib import parse
|
||||||
from six.moves.urllib import request
|
from six.moves.urllib import request
|
||||||
|
|
||||||
@@ -26,6 +27,19 @@ from heatclient import exc
|
|||||||
from heatclient.openstack.common._i18n import _
|
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,
|
def get_template_contents(template_file=None, template_url=None,
|
||||||
template_object=None, object_request=None,
|
template_object=None, object_request=None,
|
||||||
files=None, existing=False):
|
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)
|
res_base_url = res_dict.get('base_url', base_url)
|
||||||
get_file_contents(
|
get_file_contents(
|
||||||
res_dict, files, res_base_url, ignore_if)
|
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 parseractions
|
||||||
from openstackclient.common import utils
|
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.common import utils as heat_utils
|
||||||
from heatclient import exc as heat_exc
|
from heatclient import exc as heat_exc
|
||||||
from heatclient.openstack.common._i18n import _
|
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):
|
class ShowStack(show.ShowOne):
|
||||||
"""Show stack details"""
|
"""Show stack details"""
|
||||||
|
|
||||||
@@ -48,7 +185,7 @@ class ShowStack(show.ShowOne):
|
|||||||
format=parsed_args.formatter)
|
format=parsed_args.formatter)
|
||||||
|
|
||||||
|
|
||||||
def _show_stack(heat_client, stack_id, format):
|
def _show_stack(heat_client, stack_id, format='', short=False):
|
||||||
try:
|
try:
|
||||||
data = heat_client.stacks.get(stack_id=stack_id)
|
data = heat_client.stacks.get(stack_id=stack_id)
|
||||||
except heat_exc.HTTPNotFound:
|
except heat_exc.HTTPNotFound:
|
||||||
@@ -63,15 +200,21 @@ def _show_stack(heat_client, stack_id, format):
|
|||||||
'updated_time',
|
'updated_time',
|
||||||
'stack_status',
|
'stack_status',
|
||||||
'stack_status_reason',
|
'stack_status_reason',
|
||||||
'parameters',
|
|
||||||
'outputs',
|
|
||||||
'links',
|
|
||||||
]
|
]
|
||||||
exclude_columns = ('template_description',)
|
|
||||||
for key in data.to_dict():
|
if not short:
|
||||||
# add remaining columns without an explicit order
|
columns += [
|
||||||
if key not in columns and key not in exclude_columns:
|
'parameters',
|
||||||
columns.append(key)
|
'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 = {}
|
formatters = {}
|
||||||
complex_formatter = None
|
complex_formatter = None
|
||||||
if format in 'table':
|
if format in 'table':
|
||||||
|
1
heatclient/tests/test_templates/empty.yaml
Normal file
1
heatclient/tests/test_templates/empty.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
heat_template_version: 2013-05-23
|
7
heatclient/tests/test_templates/parameters.yaml
Normal file
7
heatclient/tests/test_templates/parameters.yaml
Normal file
@@ -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
|
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):
|
class TestStackShow(TestStack):
|
||||||
|
|
||||||
scenarios = [
|
scenarios = [
|
||||||
@@ -75,17 +180,26 @@ class TestStackShow(TestStack):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestStackShow, self).setUp()
|
super(TestStackShow, self).setUp()
|
||||||
self.cmd = stack.ShowStack(self.app, None)
|
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):
|
def test_stack_show(self):
|
||||||
arglist = ['--format', self.format, 'my_stack']
|
arglist = ['--format', self.format, 'my_stack']
|
||||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
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.cmd.take_action(parsed_args)
|
||||||
self.stack_client.get.assert_called_with(**{
|
self.stack_client.get.assert_called_with(**{
|
||||||
'stack_id': 'my_stack',
|
'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):
|
class TestStackList(TestStack):
|
||||||
|
|
||||||
|
@@ -116,7 +116,7 @@ def do_stack_create(hc, args):
|
|||||||
'arg2': '-t/--timeout'})
|
'arg2': '-t/--timeout'})
|
||||||
|
|
||||||
if args.pre_create:
|
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 = {
|
fields = {
|
||||||
'stack_name': args.name,
|
'stack_name': args.name,
|
||||||
@@ -139,32 +139,7 @@ def do_stack_create(hc, args):
|
|||||||
hc.stacks.create(**fields)
|
hc.stacks.create(**fields)
|
||||||
do_stack_list(hc)
|
do_stack_list(hc)
|
||||||
if args.poll is not None:
|
if args.poll is not None:
|
||||||
_poll_for_events(hc, args.name, 'CREATE', poll_period=args.poll)
|
_poll_for_events(hc, args.name, 'CREATE', 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
|
|
||||||
|
|
||||||
|
|
||||||
@utils.arg('-e', '--environment-file', metavar='<FILE or URL>',
|
@utils.arg('-e', '--environment-file', metavar='<FILE or URL>',
|
||||||
@@ -477,7 +452,7 @@ def do_stack_update(hc, args):
|
|||||||
env_paths=args.environment_file)
|
env_paths=args.environment_file)
|
||||||
|
|
||||||
if args.pre_update:
|
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 = {
|
fields = {
|
||||||
'stack_id': args.id,
|
'stack_id': args.id,
|
||||||
|
@@ -32,6 +32,7 @@ openstack.cli.extension =
|
|||||||
openstack.orchestration.v1 =
|
openstack.orchestration.v1 =
|
||||||
stack_show = heatclient.osc.v1.stack:ShowStack
|
stack_show = heatclient.osc.v1.stack:ShowStack
|
||||||
stack_list = heatclient.osc.v1.stack:ListStack
|
stack_list = heatclient.osc.v1.stack:ListStack
|
||||||
|
stack_create = heatclient.osc.v1.stack:CreateStack
|
||||||
|
|
||||||
|
|
||||||
[global]
|
[global]
|
||||||
|
Reference in New Issue
Block a user