Add stack hook poll and clear to openstack client
Refactor existing hook helper fuctions into utilites. based upon heat clis: heat hook-poll hest hook-clear Change-Id: Ib46634cc62369fb5932dcd0967ae492446c79a88 Blueprint: heat-support-python-openstackclient
This commit is contained in:
parent
a5fdf2318e
commit
69f41bceae
78
heatclient/common/hook_utils.py
Normal file
78
heatclient/common/hook_utils.py
Normal file
@ -0,0 +1,78 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
|
||||
import heatclient.exc as exc
|
||||
|
||||
from heatclient.openstack.common._i18n import _
|
||||
from heatclient.openstack.common._i18n import _LE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clear_hook(hc, stack_id, resource_name, hook_type):
|
||||
try:
|
||||
hc.resources.signal(
|
||||
stack_id=stack_id,
|
||||
resource_name=resource_name,
|
||||
data={'unset_hook': hook_type})
|
||||
except exc.HTTPNotFound:
|
||||
logger.error(
|
||||
_LE("Stack %(stack)s or resource %(resource)s"
|
||||
"not found for hook %(hook_type)"),
|
||||
{'resource': resource_name, 'stack': stack_id,
|
||||
'hook_type': hook_type})
|
||||
|
||||
|
||||
def clear_wildcard_hooks(hc, stack_id, stack_patterns, hook_type,
|
||||
resource_pattern):
|
||||
if stack_patterns:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, stack_patterns[0]):
|
||||
nested_stack = hc.resources.get(
|
||||
stack_id=stack_id,
|
||||
resource_name=res_name)
|
||||
clear_wildcard_hooks(
|
||||
hc,
|
||||
nested_stack.physical_resource_id,
|
||||
stack_patterns[1:], hook_type, resource_pattern)
|
||||
else:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, resource_pattern):
|
||||
clear_hook(hc, stack_id, res_name, hook_type)
|
||||
|
||||
|
||||
def get_hook_type_via_status(hc, stack_id):
|
||||
# Figure out if the hook should be pre-create or pre-update based
|
||||
# on the stack status, also sanity assertions that we're in-progress.
|
||||
try:
|
||||
stack = hc.stacks.get(stack_id=stack_id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s') % stack_id)
|
||||
else:
|
||||
if 'IN_PROGRESS' not in stack.stack_status:
|
||||
raise exc.CommandError(_('Stack status %s not IN_PROGRESS') %
|
||||
stack.stack_status)
|
||||
|
||||
if 'CREATE' in stack.stack_status:
|
||||
hook_type = 'pre-create'
|
||||
elif 'UPDATE' in stack.stack_status:
|
||||
hook_type = 'pre-update'
|
||||
else:
|
||||
raise exc.CommandError(_('Unexpected stack status %s, '
|
||||
'only create/update supported')
|
||||
% stack.stack_status)
|
||||
return hook_type
|
@ -26,7 +26,9 @@ from oslo_serialization import jsonutils
|
||||
import six
|
||||
from six.moves.urllib import request
|
||||
|
||||
from heatclient.common import event_utils
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.common import hook_utils
|
||||
from heatclient.common import http
|
||||
from heatclient.common import template_utils
|
||||
from heatclient.common import utils as heat_utils
|
||||
@ -1060,3 +1062,134 @@ class CheckStack(StackActionBase):
|
||||
['check_complete'],
|
||||
['check_failed']
|
||||
)
|
||||
|
||||
|
||||
class StackHookPoll(lister.Lister):
|
||||
'''List resources with pending hook for a stack.'''
|
||||
|
||||
log = logging.getLogger(__name__ + '.StackHookPoll')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(StackHookPoll, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Stack to display (name or ID)')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nested-depth',
|
||||
metavar='<nested-depth>',
|
||||
help=_('Depth of nested stacks from which to display hooks')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _hook_poll(
|
||||
parsed_args,
|
||||
heat_client
|
||||
)
|
||||
|
||||
|
||||
def _hook_poll(args, heat_client):
|
||||
"""List resources with pending hook for a stack."""
|
||||
|
||||
# There are a few steps to determining if a stack has pending hooks
|
||||
# 1. The stack is IN_PROGRESS status (otherwise, by definition no hooks
|
||||
# can be pending
|
||||
# 2. There is an event for a resource associated with hitting a hook
|
||||
# 3. There is not an event associated with clearing the hook in step(2)
|
||||
#
|
||||
# So, essentially, this ends up being a specially filtered type of event
|
||||
# listing, because all hook status is exposed via events. In future
|
||||
# we might consider exposing some more efficient interface via the API
|
||||
# to reduce the expense of this brute-force polling approach
|
||||
columns = ['ID', 'Resource Status Reason', 'Resource Status', 'Event Time']
|
||||
|
||||
if args.nested_depth:
|
||||
try:
|
||||
nested_depth = int(args.nested_depth)
|
||||
except ValueError:
|
||||
msg = _("--nested-depth invalid value %s") % args.nested_depth
|
||||
raise exc.CommandError(msg)
|
||||
columns.append('Stack Name')
|
||||
else:
|
||||
nested_depth = 0
|
||||
|
||||
hook_type = hook_utils.get_hook_type_via_status(heat_client, args.stack)
|
||||
event_args = {'sort_dir': 'asc'}
|
||||
hook_events = event_utils.get_hook_events(
|
||||
heat_client, stack_id=args.stack, event_args=event_args,
|
||||
nested_depth=nested_depth, hook_type=hook_type)
|
||||
|
||||
if len(hook_events) >= 1:
|
||||
if hasattr(hook_events[0], 'resource_name'):
|
||||
columns.insert(0, 'Resource Name')
|
||||
else:
|
||||
columns.insert(0, 'Logical Resource ID')
|
||||
|
||||
rows = (utils.get_item_properties(h, columns) for h in hook_events)
|
||||
return (columns, rows)
|
||||
|
||||
|
||||
class StackHookClear(command.Command):
|
||||
"""Clear resource hooks on a given stack."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.StackHookClear')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(StackHookClear, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Stack to display (name or ID)')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pre-create',
|
||||
action='store_true',
|
||||
help=_('Clear the pre-create hooks')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pre-update',
|
||||
action='store_true',
|
||||
help=_('Clear the pre-update hooks')
|
||||
)
|
||||
parser.add_argument(
|
||||
'hook',
|
||||
metavar='<resource>',
|
||||
nargs='+',
|
||||
help=_('Resource names with hooks to clear. 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')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _hook_clear(
|
||||
parsed_args,
|
||||
heat_client
|
||||
)
|
||||
|
||||
|
||||
def _hook_clear(args, heat_client):
|
||||
"""Clear resource hooks on a given stack."""
|
||||
if args.pre_create:
|
||||
hook_type = 'pre-create'
|
||||
elif args.pre_update:
|
||||
hook_type = 'pre-update'
|
||||
else:
|
||||
hook_type = hook_utils.get_hook_type_via_status(heat_client,
|
||||
args.stack)
|
||||
|
||||
for hook_string in args.hook:
|
||||
hook = [b for b in hook_string.split('/') if b]
|
||||
resource_pattern = hook[-1]
|
||||
stack_id = args.stack
|
||||
|
||||
hook_utils.clear_wildcard_hooks(heat_client, stack_id, hook[:-1],
|
||||
hook_type, resource_pattern)
|
||||
|
@ -26,6 +26,8 @@ from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import stack
|
||||
from heatclient.tests import inline_templates
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import events
|
||||
from heatclient.v1 import resources
|
||||
from heatclient.v1 import stacks
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
@ -1074,3 +1076,139 @@ class TestStackCheck(_TestStackCheckBase, TestStack):
|
||||
|
||||
def test_stack_check_exception(self):
|
||||
self._test_stack_action_exception()
|
||||
|
||||
|
||||
class TestStackHookPoll(TestStack):
|
||||
|
||||
stack = stacks.Stack(None, {
|
||||
"id": '1234',
|
||||
"stack_name": 'my_stack',
|
||||
"creation_time": "2013-08-04T20:57:55Z",
|
||||
"updated_time": "2013-08-04T20:57:55Z",
|
||||
"stack_status": "CREATE_IN_PROGRESS"
|
||||
})
|
||||
resource = resources.Resource(None, {
|
||||
'resource_name': 'resource1',
|
||||
'links': [{'href': 'http://heat.example.com:8004/resource1',
|
||||
'rel': 'self'},
|
||||
{'href': 'http://192.168.27.100:8004/my_stack',
|
||||
'rel': 'stack'}],
|
||||
'logical_resource_id': 'random_group',
|
||||
'creation_time': '2015-12-03T16:50:56',
|
||||
'resource_status': 'INIT_COMPLETE',
|
||||
'updated_time': '2015-12-03T16:50:56',
|
||||
'required_by': [],
|
||||
'resource_status_reason': '',
|
||||
'physical_resource_id': '',
|
||||
'resource_type': 'OS::Heat::ResourceGroup',
|
||||
'id': '1111'
|
||||
})
|
||||
columns = ['ID', 'Resource Status Reason', 'Resource Status',
|
||||
'Event Time']
|
||||
event0 = events.Event(manager=None, info={
|
||||
'resource_name': 'my_stack',
|
||||
'event_time': '2015-12-02T16:50:56',
|
||||
'logical_resource_id': 'my_stack',
|
||||
'resource_status': 'CREATE_IN_PROGRESS',
|
||||
'resource_status_reason': 'Stack CREATE started',
|
||||
'id': '1234'
|
||||
})
|
||||
event1 = events.Event(manager=None, info={
|
||||
'resource_name': 'resource1',
|
||||
'event_time': '2015-12-03T19:59:58',
|
||||
'logical_resource_id': 'resource1',
|
||||
'resource_status': 'INIT_COMPLETE',
|
||||
'resource_status_reason':
|
||||
'CREATE paused until Hook pre-create is cleared',
|
||||
'id': '1111'
|
||||
})
|
||||
row1 = ('resource1',
|
||||
'1111',
|
||||
'CREATE paused until Hook pre-create is cleared',
|
||||
'INIT_COMPLETE',
|
||||
'2015-12-03T19:59:58'
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackHookPoll, self).setUp()
|
||||
self.cmd = stack.StackHookPoll(self.app, None)
|
||||
self.mock_client.stacks.get = mock.Mock(
|
||||
return_value=self.stack)
|
||||
self.mock_client.events.list = mock.Mock(
|
||||
return_value=[self.event0, self.event1])
|
||||
self.mock_client.resources.list = mock.Mock(
|
||||
return_value=[self.resource])
|
||||
|
||||
def test_hook_poll(self):
|
||||
expected_columns = ['Resource Name'] + self.columns
|
||||
expected_rows = [self.row1]
|
||||
arglist = ['my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.assertEqual(expected_rows, list(rows))
|
||||
self.assertEqual(expected_columns, columns)
|
||||
|
||||
def test_hook_poll_nested(self):
|
||||
expected_columns = ['Resource Name'] + self.columns + ['Stack Name']
|
||||
expected_rows = [self.row1 + ('my_stack',)]
|
||||
arglist = ['my_stack', '--nested-depth=10']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.assertEqual(expected_rows, list(rows))
|
||||
self.assertEqual(expected_columns, columns)
|
||||
|
||||
def test_hook_poll_nested_invalid(self):
|
||||
arglist = ['my_stack', '--nested-depth=ugly']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
||||
class TestStackHookClear(TestStack):
|
||||
|
||||
stack = stacks.Stack(None, {
|
||||
"id": '1234',
|
||||
"stack_name": 'my_stack',
|
||||
"creation_time": "2013-08-04T20:57:55Z",
|
||||
"updated_time": "2013-08-04T20:57:55Z",
|
||||
"stack_status": "CREATE_IN_PROGRESS"
|
||||
})
|
||||
resource = resources.Resource(None, {
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'resource'
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackHookClear, self).setUp()
|
||||
self.cmd = stack.StackHookClear(self.app, None)
|
||||
self.mock_client.stacks.get = mock.Mock(
|
||||
return_value=self.stack)
|
||||
self.mock_client.resources.signal = mock.Mock()
|
||||
self.mock_client.resources.list = mock.Mock(
|
||||
return_value=[self.resource])
|
||||
|
||||
def test_hook_clear(self):
|
||||
arglist = ['my_stack', 'resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resources.signal.assert_called_once_with(
|
||||
data={'unset_hook': 'pre-create'},
|
||||
resource_name='resource',
|
||||
stack_id='my_stack')
|
||||
|
||||
def test_hook_clear_pre_create(self):
|
||||
arglist = ['my_stack', 'resource', '--pre-create']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resources.signal.assert_called_once_with(
|
||||
data={'unset_hook': 'pre-create'},
|
||||
resource_name='resource',
|
||||
stack_id='my_stack')
|
||||
|
||||
def test_hook_clear_pre_update(self):
|
||||
arglist = ['my_stack', 'resource', '--pre-update']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resources.signal.assert_called_once_with(
|
||||
data={'unset_hook': 'pre-update'},
|
||||
resource_name='resource',
|
||||
stack_id='my_stack')
|
||||
|
@ -16,6 +16,8 @@ import testtools
|
||||
|
||||
import heatclient.v1.shell as shell
|
||||
|
||||
from heatclient.common import hook_utils
|
||||
|
||||
|
||||
class TestHooks(testtools.TestCase):
|
||||
def setUp(self):
|
||||
@ -220,7 +222,8 @@ class TestHooks(testtools.TestCase):
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_clear_all_hooks(self):
|
||||
shell._get_hook_type_via_status = mock.Mock(return_value='pre-create')
|
||||
hook_utils.get_hook_type_via_status = mock.Mock(
|
||||
return_value='pre-create')
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['bp'])
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=True)
|
||||
|
@ -13,7 +13,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
@ -25,13 +24,13 @@ import yaml
|
||||
|
||||
from heatclient.common import deployment_utils
|
||||
from heatclient.common import event_utils
|
||||
from heatclient.common import hook_utils
|
||||
from heatclient.common import http
|
||||
from heatclient.common import template_format
|
||||
from heatclient.common import template_utils
|
||||
from heatclient.common import utils
|
||||
|
||||
from heatclient.openstack.common._i18n import _
|
||||
from heatclient.openstack.common._i18n import _LE
|
||||
from heatclient.openstack.common._i18n import _LW
|
||||
|
||||
import heatclient.exc as exc
|
||||
@ -993,42 +992,15 @@ def do_hook_clear(hc, args):
|
||||
elif args.pre_update:
|
||||
hook_type = 'pre-update'
|
||||
else:
|
||||
hook_type = _get_hook_type_via_status(hc, args.id)
|
||||
hook_type = hook_utils.get_hook_type_via_status(hc, args.id)
|
||||
|
||||
for hook_string in args.hook:
|
||||
hook = [b for b in hook_string.split('/') if b]
|
||||
resource_pattern = hook[-1]
|
||||
stack_id = args.id
|
||||
|
||||
def clear_hook(stack_id, resource_name):
|
||||
try:
|
||||
hc.resources.signal(
|
||||
stack_id=stack_id,
|
||||
resource_name=resource_name,
|
||||
data={'unset_hook': hook_type})
|
||||
except exc.HTTPNotFound:
|
||||
logger.error(
|
||||
_LE("Stack %(stack)s or resource %(resource)s not found"),
|
||||
{'resource': resource_name, 'stack': stack_id})
|
||||
|
||||
def clear_wildcard_hooks(stack_id, stack_patterns):
|
||||
if stack_patterns:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, stack_patterns[0]):
|
||||
nested_stack = hc.resources.get(
|
||||
stack_id=stack_id,
|
||||
resource_name=res_name)
|
||||
clear_wildcard_hooks(
|
||||
nested_stack.physical_resource_id,
|
||||
stack_patterns[1:])
|
||||
else:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, resource_pattern):
|
||||
clear_hook(stack_id, res_name)
|
||||
|
||||
clear_wildcard_hooks(stack_id, hook[:-1])
|
||||
hook_utils.clear_wildcard_hooks(hc, stack_id, hook[:-1],
|
||||
hook_type, resource_pattern)
|
||||
|
||||
|
||||
@utils.arg('id', metavar='<NAME or ID>',
|
||||
@ -1096,29 +1068,6 @@ def do_event_list(hc, args):
|
||||
utils.print_list(events, display_fields, sortby_index=None)
|
||||
|
||||
|
||||
def _get_hook_type_via_status(hc, stack_id):
|
||||
# Figure out if the hook should be pre-create or pre-update based
|
||||
# on the stack status, also sanity assertions that we're in-progress.
|
||||
try:
|
||||
stack = hc.stacks.get(stack_id=stack_id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s') % stack_id)
|
||||
else:
|
||||
if 'IN_PROGRESS' not in stack.stack_status:
|
||||
raise exc.CommandError(_('Stack status %s not IN_PROGRESS') %
|
||||
stack.stack_status)
|
||||
|
||||
if 'CREATE' in stack.stack_status:
|
||||
hook_type = 'pre-create'
|
||||
elif 'UPDATE' in stack.stack_status:
|
||||
hook_type = 'pre-update'
|
||||
else:
|
||||
raise exc.CommandError(_('Unexpected stack status %s, '
|
||||
'only create/update supported')
|
||||
% stack.stack_status)
|
||||
return hook_type
|
||||
|
||||
|
||||
@utils.arg('id', metavar='<NAME or ID>',
|
||||
help=_('Name or ID of stack to show the pending hooks for.'))
|
||||
@utils.arg('-n', '--nested-depth', metavar='<DEPTH>',
|
||||
@ -1148,7 +1097,7 @@ def do_hook_poll(hc, args):
|
||||
else:
|
||||
nested_depth = 0
|
||||
|
||||
hook_type = _get_hook_type_via_status(hc, args.id)
|
||||
hook_type = hook_utils.get_hook_type_via_status(hc, args.id)
|
||||
event_args = {'sort_dir': 'asc'}
|
||||
hook_events = event_utils.get_hook_events(
|
||||
hc, stack_id=args.id, event_args=event_args,
|
||||
|
@ -49,6 +49,8 @@ openstack.orchestration.v1 =
|
||||
stack_delete = heatclient.osc.v1.stack:DeleteStack
|
||||
stack_event_list = heatclient.osc.v1.event:ListEvent
|
||||
stack_event_show = heatclient.osc.v1.event:ShowEvent
|
||||
stack_hook_clear = heatclient.osc.v1.stack:StackHookClear
|
||||
stack_hook_poll = heatclient.osc.v1.stack:StackHookPoll
|
||||
stack_list = heatclient.osc.v1.stack:ListStack
|
||||
stack_output_list = heatclient.osc.v1.stack:OutputListStack
|
||||
stack_output_show = heatclient.osc.v1.stack:OutputShowStack
|
||||
|
Loading…
Reference in New Issue
Block a user