# Copyright 2020 Mirantis 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. """ Heat Client """ #import sys import time from heatclient import exc from oslo_log import log as logging from timeout_decorator import timeout LOG = logging.getLogger(__name__) def create_stack(heat_client, stack_name, template, parameters, environment=None): """ Create Stack """ stack_params = { 'stack_name': stack_name, 'template': template, 'parameters': parameters, 'environment': environment, } stack = heat_client.stacks.create(**stack_params)['stack'] LOG.info('New stack: %s', stack) wait_stack_completion(heat_client, stack['id']) return stack['id'] def get_stack_status(heat_client, stack_id): """ Get Stack Status """ # stack.get operation may take long time and run out of time. The reason # is that it resolves all outputs which is done serially. On the other hand # stack status can be retrieved from the list operation. Internally listing # supports paging and every request should not take too long. for stack in heat_client.stacks.list(): if stack.id == stack_id: return stack.status, stack.stack_status_reason else: raise exc.HTTPNotFound(message='Stack %s is not found' % stack_id) return None def get_id_with_name(heat_client, stack_name): """ Get Stack ID by name """ # This method isn't really necessary since the Heat client accepts # stack_id and stack_name interchangeably. This is provided more as a # safety net to use ids which are guaranteed to be unique and provides # the benefit of keeping the Shaker code consistent and more easily # traceable. stack = heat_client.stacks.get(stack_name) return stack.id def wait_stack_completion(heat_client, stack_id): """ Wait for Stack completion """ reason = None status = None while True: status, reason = get_stack_status(heat_client, stack_id) LOG.debug('Stack status: %s', status) if status not in ['IN_PROGRESS', '']: break time.sleep(5) if status != 'COMPLETE': resources = heat_client.resources.list(stack_id) for res in resources: if (res.resource_status != 'CREATE_COMPLETE' and res.resource_status_reason): LOG.error('Heat stack resource %(res)s of type %(type)s ' 'failed with %(reason)s', dict(res=res.logical_resource_id, type=res.resource_type, reason=res.resource_status_reason)) raise exc.StackFailure(stack_id, status, reason) # set the timeout for this method so we don't get stuck polling indefinitely # waiting for a delete @timeout(600) def wait_stack_deletion(heat_client, stack_id): """ Wait for stack deletion """ try: heat_client.stacks.delete(stack_id) while True: status, reason = get_stack_status(heat_client, stack_id) LOG.debug('Stack status: %s Stack reason: %s', status, reason) if status == 'FAILED': raise exc.StackFailure('Failed to delete stack %s' % stack_id) time.sleep(5) except TimeoutError: LOG.error('Timed out waiting for deletion of stack %s' % stack_id) except exc.HTTPNotFound: # once the stack is gone we can assume it was successfully deleted # clear the exception so it doesn't confuse the logs #if sys.version_info < (3, 0): # sys.exc_clear() LOG.info('Stack %s was successfully deleted', stack_id) def get_stack_outputs(heat_client, stack_id): """ Get Stack Output """ # try to use optimized way to retrieve outputs, fallback otherwise if hasattr(heat_client.stacks, 'output_list'): try: output_list = heat_client.stacks.output_list(stack_id)['outputs'] result = {} for output in output_list: output_key = output['output_key'] value = heat_client.stacks.output_show(stack_id, output_key) result[output_key] = value['output']['output_value'] return result except BaseException as err: LOG.info('Cannot get output list, fallback to old way: %s', err) outputs_list = heat_client.stacks.get(stack_id).to_dict()['outputs'] return dict((item['output_key'], item['output_value']) for item in outputs_list)