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:
Mark Vanderwiel 2015-12-02 12:07:57 -06:00
parent a5fdf2318e
commit 69f41bceae
6 changed files with 360 additions and 57 deletions

View 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

View File

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

View File

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

View File

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

View File

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

View File

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