You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
10 KiB
Python
265 lines
10 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._i18n import _
|
|
from heatclient.common import utils
|
|
import heatclient.exc as exc
|
|
from heatclient.v1 import events as events_mod
|
|
|
|
|
|
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_name_from_links(event):
|
|
links = dict((l.get('rel'),
|
|
l.get('href')) for l in getattr(event, 'links', []))
|
|
href = links.get('stack')
|
|
if not href:
|
|
return
|
|
return href.split('/stacks/', 1)[-1].split('/')[0]
|
|
|
|
|
|
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:
|
|
stack_name = stack_id.split("/")[0]
|
|
# Show which stack the event comes from (for nested events)
|
|
for e in events:
|
|
e.stack_name = _get_stack_name_from_links(e) or stack_name
|
|
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 # noqa: E731
|
|
else:
|
|
stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED') # noqa E731
|
|
|
|
no_event_polls = 0
|
|
msg_template = _("\n Stack %(name)s %(status)s \n")
|
|
if not out:
|
|
out = sys.stdout
|
|
event_log_context = utils.EventLogContext()
|
|
|
|
def is_stack_event(event):
|
|
if getattr(event, 'resource_name', '') != stack_name:
|
|
return False
|
|
|
|
phys_id = getattr(event, 'physical_resource_id', '')
|
|
links = dict((l.get('rel'),
|
|
l.get('href')) for l in getattr(event, 'links', []))
|
|
stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1]
|
|
return stack_id == phys_id
|
|
|
|
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 is_stack_event(event):
|
|
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, resolve_outputs=False)
|
|
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)
|
|
|
|
|
|
def wait_for_events(ws, stack_name, out=None):
|
|
"""Receive events over the passed websocket and wait for final status."""
|
|
msg_template = _("\n Stack %(name)s %(status)s \n")
|
|
if not out:
|
|
out = sys.stdout
|
|
event_log_context = utils.EventLogContext()
|
|
while True:
|
|
data = ws.recv()['body']
|
|
event = events_mod.Event(None, data['payload'], True)
|
|
# Keep compatibility with the HTTP API
|
|
event.event_time = data['timestamp']
|
|
event.resource_status = '%s_%s' % (event.resource_action,
|
|
event.resource_status)
|
|
events_log = utils.event_log_formatter([event], event_log_context)
|
|
out.write(events_log)
|
|
out.write('\n')
|
|
if data['payload']['resource_name'] == stack_name:
|
|
stack_status = data['payload']['resource_status']
|
|
if stack_status in ('COMPLETE', 'FAILED'):
|
|
msg = msg_template % dict(
|
|
name=stack_name, status=event.resource_status)
|
|
return '%s_%s' % (event.resource_action, stack_status), msg
|