# 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_links = [link for link in first_links if link.get('rel') == 'root_stack'] if root_stack_links: # 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 = {link.get('rel'): link.get('href') for link 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 = {link.get('rel'): link.get('href') for link 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