Files
python-heatclient/heatclient/common/event_utils.py
Steve Baker b28d8c6101 Show resource name path in event log formatter
The resource name path was introduced in the 'openstack stack failures'
command and is a user-friendly way of referring to a deeply nested
resource.

This change modifies the event log formatter to build and display the
resource name path for each event based only off data in previous
events.

For building the full resource path, and omitting ugly stack names
from the resource path, the following heat change is needed
Ib8feb7752bd5736785d142216312bb35629b3601. However the output is still
improved slightly when running against an older heat which does not
have this change.

Change-Id: I86b687fc516da215a6baeef8b365a520d12a8ab1
Closes-Bug: #1619415
2016-09-06 13:54:12 +12:00

219 lines
8.4 KiB
Python

# Copyright 2015 Red Hat Inc.
#
# 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 sys
import time
from heatclient.common import utils
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'
elif hook_type == 'pre-delete':
stack_action_reason = 'Stack DELETE started'
hook_event_reason = 'DELETE paused until Hook pre-delete is cleared'
hook_clear_event_reason = 'Hook pre-delete 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):
event_args = dict(event_args)
if marker:
event_args['marker'] = marker
if limit:
event_args['limit'] = limit
if not nested_depth:
# simple call with no nested_depth
return _get_stack_events(hc, stack_id, event_args)
# assume an API which supports nested_depth
event_args['nested_depth'] = nested_depth
events = _get_stack_events(hc, stack_id, event_args)
if not events:
return events
first_links = getattr(events[0], 'links', [])
root_stack_link = [l for l in first_links
if l.get('rel') == 'root_stack']
if root_stack_link:
# response has a root_stack link, indicating this is an API which
# supports nested_depth
return events
# API doesn't support nested_depth, do client-side paging and recursive
# event fetch
marker = event_args.pop('marker', None)
limit = event_args.pop('limit', None)
event_args.pop('nested_depth', None)
events = _get_stack_events(hc, stack_id, event_args)
events.extend(_get_nested_events(hc, nested_depth,
stack_id, event_args))
# Because there have been multiple stacks events mangled into
# one list, we need to sort before passing to print_list
# Note we can't use the prettytable sortby_index here, because
# the "start" option doesn't allow post-sort slicing, which
# will be needed to make "--marker" work for nested_depth lists
events.sort(key=lambda x: x.event_time)
# Slice the list if marker is specified
if marker:
try:
marker_index = [e.id for e in events].index(marker)
events = events[marker_index:]
except ValueError:
pass
# Slice the list if limit is specified
if limit:
limit_index = min(int(limit), len(events))
events = events[:limit_index]
return events
def _get_nested_ids(hc, stack_id):
nested_ids = []
try:
resources = hc.resources.list(stack_id=stack_id)
except exc.HTTPNotFound:
raise exc.CommandError(_('Stack not found: %s') % stack_id)
for r in resources:
nested_id = utils.resource_nested_identifier(r)
if nested_id:
nested_ids.append(nested_id)
return nested_ids
def _get_nested_events(hc, nested_depth, stack_id, event_args):
# FIXME(shardy): this is very inefficient, we should add nested_depth to
# the event_list API in a future heat version, but this will be required
# until kilo heat is EOL.
nested_ids = _get_nested_ids(hc, stack_id)
nested_events = []
for n_id in nested_ids:
stack_events = _get_stack_events(hc, n_id, event_args)
if stack_events:
nested_events.extend(stack_events)
if nested_depth > 1:
next_depth = nested_depth - 1
nested_events.extend(_get_nested_events(
hc, next_depth, n_id, event_args))
return nested_events
def _get_stack_events(hc, stack_id, event_args):
event_args['stack_id'] = stack_id
try:
events = hc.events.list(**event_args)
except exc.HTTPNotFound as ex:
# it could be the stack or resource that is not found
# just use the message that the server sent us.
raise exc.CommandError(str(ex))
else:
# Show which stack the event comes from (for nested events)
for e in events:
e.stack_name = stack_id.split("/")[0]
return events
def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None,
out=None, nested_depth=0):
"""Continuously poll events and logs for performed action on stack."""
if action:
stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action)
stop_check = lambda a: a in stop_status
else:
stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED')
no_event_polls = 0
msg_template = _("\n Stack %(name)s %(status)s \n")
if not out:
out = sys.stdout
event_log_context = utils.EventLogContext()
while True:
events = get_events(hc, stack_id=stack_name, nested_depth=nested_depth,
event_args={'sort_dir': 'asc',
'marker': marker})
if len(events) == 0:
no_event_polls += 1
else:
no_event_polls = 0
# set marker to last event that was received.
marker = getattr(events[-1], 'id', None)
events_log = utils.event_log_formatter(events, event_log_context)
out.write(events_log)
out.write('\n')
for event in events:
# check if stack event was also received
if getattr(event, 'resource_name', '') == stack_name:
stack_status = getattr(event, 'resource_status', '')
msg = msg_template % dict(
name=stack_name, status=stack_status)
if stop_check(stack_status):
return stack_status, msg
if no_event_polls >= 2:
# after 2 polls with no events, fall back to a stack get
stack = hc.stacks.get(stack_name)
stack_status = stack.stack_status
msg = msg_template % dict(
name=stack_name, status=stack_status)
if stop_check(stack_status):
return stack_status, msg
# go back to event polling again
no_event_polls = 0
time.sleep(poll_period)