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:
@@ -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)
|
||||
|
@@ -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):
|
||||
|
@@ -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>',
|
||||
|
Reference in New Issue
Block a user