Add hook-poll function to check if a stack has pending hooks

Currently using the breakpoints/hooks functionality on large trees of
stacks is a bit inconvenient, because you may set hooks in nested stacks
so the events aren't easily visible via the top-level event interfaces.

This has been addressed via the new --nested-depth option to event-list,
but it's still hard to determine (programatically e.g from a script) if
there's pending hook which requires clearing.

So this patch introduces an initial implementation of a specialized
event-list function "hook-poll", which filters the events and displays
only those events which have pending hooks (e.g those which have yet
to be signalled to clear them).

I expect the efficiency of the implementation can be much improved in the
future if we add a propert nested_depth argument to the heat events API,
and/or add a hook API which enables easier introspection of hook status.
The CLI interface should be reasonable in the event such rework happens,
but the current (slow) implementation will work with the API interfaces
we have available now in kilo Heat.

Change-Id: I71b19202ab29f44e5c09b4ee04be4aeaea038c28
This commit is contained in:
Steven Hardy
2015-04-15 12:06:10 -04:00
parent ffa0f0f58f
commit 53c46f0b34
3 changed files with 237 additions and 1 deletions

View File

@@ -18,6 +18,46 @@ import heatclient.exc as exc
from heatclient.openstack.common._i18n import _
def get_hook_events(hc, stack_id, event_args, nested_depth=0,
hook_type='pre-create'):
if hook_type == 'pre-create':
stack_action_reason = 'Stack CREATE started'
hook_event_reason = 'CREATE paused until Hook pre-create is cleared'
hook_clear_event_reason = 'Hook pre-create is cleared'
elif hook_type == 'pre-update':
stack_action_reason = 'Stack UPDATE started'
hook_event_reason = 'UPDATE paused until Hook pre-update is cleared'
hook_clear_event_reason = 'Hook pre-update is cleared'
else:
raise exc.CommandError(_('Unexpected hook type %s') % hook_type)
events = get_events(hc, stack_id=stack_id, event_args=event_args,
nested_depth=nested_depth)
# Get the most recent event associated with this action, which gives us the
# event when we moved into IN_PROGRESS for the hooks we're interested in.
stack_name = stack_id.split("/")[0]
action_start_event = [e for e in enumerate(events)
if e[1].resource_status_reason == stack_action_reason
and e[1].stack_name == stack_name][-1]
# Slice the events with the index from the enumerate
action_start_index = action_start_event[0]
events = events[action_start_index:]
# Get hook events still pending by some list filtering/comparison
# We build a map hook events per-resource, and remove any event
# for which there is a corresponding hook-clear event.
resource_event_map = {}
for e in events:
stack_resource = (e.stack_name, e.resource_name)
if e.resource_status_reason == hook_event_reason:
resource_event_map[(e.stack_name, e.resource_name)] = e
elif e.resource_status_reason == hook_clear_event_reason:
if resource_event_map.get(stack_resource):
del(resource_event_map[(e.stack_name, e.resource_name)])
return list(resource_event_map.values())
def get_events(hc, stack_id, event_args, nested_depth=0,
marker=None, limit=None):
events = _get_stack_events(hc, stack_id, event_args)

View File

@@ -2417,7 +2417,6 @@ class ShellTestEventsNested(ShellBase):
'stack_name', 'teststack', 'nested']
for r in required:
self.assertRegexpMatches(list_text, r)
self.assertNotRegexpMatches(list_text, 'p_eventid2')
self.assertNotRegexpMatches(list_text, 'n_eventid2')
@@ -2425,6 +2424,141 @@ class ShellTestEventsNested(ShellBase):
"%s.*\n.*%s.*\n" % timestamps[:2])
class ShellTestHookPoll(ShellBase):
def setUp(self):
super(ShellTestHookPoll, self).setUp()
self.set_fake_env(FAKE_ENV_KEYSTONE_V2)
def _stub_stack_response(self, stack_id, action='CREATE',
status='IN_PROGRESS'):
# Stub parent stack show for status
resp_dict = {"stack": {
"id": stack_id.split("/")[1],
"stack_name": stack_id.split("/")[0],
"stack_status": '%s_%s' % (action, status),
"creation_time": "2014-01-06T16:14:00Z",
}}
resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict))
def _stub_responses(self, stack_id, nested_id, action='CREATE'):
action_reason = 'Stack %s started' % action
hook_reason = ('%s paused until Hook pre-%s is cleared' %
(action, action.lower()))
hook_clear_reason = 'Hook pre-%s is cleared' % action.lower()
self._stub_stack_response(stack_id, action)
# Stub events for parent stack
ev_resp_dict = {"events": [{"id": "p_eventid1",
"event_time": "2014-01-06T16:14:00Z",
"resource_name": None,
"resource_status_reason": action_reason},
{"id": "p_eventid2",
"event_time": "2014-01-06T16:17:00Z",
"resource_name": "p_res",
"resource_status_reason": hook_reason}]}
ev_resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(ev_resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/%s/events?sort_dir=asc' % (
stack_id)).AndReturn((ev_resp, ev_resp_dict))
# Stub resources for parent, including one nested
res_resp_dict = {"resources": [
{"links": [{"href": "http://heat/foo", "rel": "self"},
{"href": "http://heat/foo2",
"rel": "resource"},
{"href": "http://heat/%s" % nested_id,
"rel": "nested"}],
"resource_type": "OS::Nested::Foo"}]}
res_resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(res_resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/%s/resources' % (
stack_id)).AndReturn((res_resp, res_resp_dict))
# Stub the events for the nested stack
nev_resp_dict = {"events": [{"id": 'n_eventid1',
"event_time": "2014-01-06T16:15:00Z",
"resource_name": "n_res",
"resource_status_reason": hook_reason},
{"id": 'n_eventid2',
"event_time": "2014-01-06T16:16:00Z",
"resource_name": "n_res",
"resource_status_reason":
hook_clear_reason}]}
nev_resp = fakes.FakeHTTPResponse(
200,
'OK',
{'content-type': 'application/json'},
jsonutils.dumps(nev_resp_dict))
http.HTTPClient.json_request(
'GET', '/stacks/%s/events?sort_dir=asc' % (
nested_id)).AndReturn((nev_resp, nev_resp_dict))
def test_hook_poll_pre_create(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_id = 'nested/2'
self._stub_responses(stack_id, nested_id, 'CREATE')
self.m.ReplayAll()
list_text = self.shell('hook-poll %s --nested-depth 1' % stack_id)
hook_reason = 'CREATE paused until Hook pre-create is cleared'
required = ['id', 'p_eventid2', 'stack_name', 'teststack', hook_reason]
for r in required:
self.assertRegexpMatches(list_text, r)
self.assertNotRegexpMatches(list_text, 'p_eventid1')
self.assertNotRegexpMatches(list_text, 'n_eventid1')
self.assertNotRegexpMatches(list_text, 'n_eventid2')
def test_hook_poll_pre_update(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_id = 'nested/2'
self._stub_responses(stack_id, nested_id, 'UPDATE')
self.m.ReplayAll()
list_text = self.shell('hook-poll %s --nested-depth 1' % stack_id)
hook_reason = 'UPDATE paused until Hook pre-update is cleared'
required = ['id', 'p_eventid2', 'stack_name', 'teststack', hook_reason]
for r in required:
self.assertRegexpMatches(list_text, r)
self.assertNotRegexpMatches(list_text, 'p_eventid1')
self.assertNotRegexpMatches(list_text, 'n_eventid1')
self.assertNotRegexpMatches(list_text, 'n_eventid2')
def test_hook_poll_bad_status(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
self._stub_stack_response(stack_id, status='COMPLETE')
self.m.ReplayAll()
error = self.assertRaises(
exc.CommandError, self.shell,
'hook-poll %s --nested-depth 1' % stack_id)
self.assertIn('Stack status CREATE_COMPLETE not IN_PROGRESS',
str(error))
def test_shell_nested_depth_invalid_value(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
self.m.ReplayAll()
error = self.assertRaises(
exc.CommandError, self.shell,
'hook-poll %s --nested-depth Z' % stack_id)
self.assertIn('--nested-depth invalid value Z', str(error))
class ShellTestResources(ShellBase):
def setUp(self):

View File

@@ -940,6 +940,68 @@ def do_event_list(hc, args):
utils.print_list(events, display_fields, sortby_index=None)
@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>',
help=_('Depth of nested stacks from which to display hooks.'))
def do_hook_poll(hc, args):
'''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
display_fields = ['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)
display_fields.append('stack_name')
else:
nested_depth = 0
try:
stack = hc.stacks.get(stack_id=args.id)
except exc.HTTPNotFound:
raise exc.CommandError(_('Stack not found: %s') % args.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_action)
stack_id = args.id
event_args = {'sort_dir': 'asc'}
hook_events = event_utils.get_hook_events(
hc, stack_id=stack_id, event_args=event_args,
nested_depth=nested_depth, hook_type=hook_type)
if len(hook_events) >= 1:
if hasattr(hook_events[0], 'resource_name'):
display_fields.insert(0, 'resource_name')
else:
display_fields.insert(0, 'logical_resource_id')
utils.print_list(hook_events, display_fields, sortby_index=None)
@utils.arg('id', metavar='<NAME or ID>',
help=_('Name or ID of stack to show the events for.'))
@utils.arg('resource', metavar='<RESOURCE>',