# # All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or # its licensors. # # For complete copyright and license terms please see the LICENSE at the root of this # distribution (the "License"). All use of this software is governed by the License, # or, if provided, by the license below or the license accompanying this file. Do not # remove or modify any license notices. This file is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # import botocore from botocore.exceptions import ClientError from . import cleanup_utils from . import exception_utils def delete_cf_stacks(cleaner): """ Call CloudFormation to delete the stack. We make use of CloudFormation's Waiter to check the status of the stack (https://boto3.readthedocs.io/en/latest/reference/services/cloudformation.html#CloudFormation.Waiter.StackDeleteComplete) :param cleaner: A Cleaner object from the main cleanup.py script :return: None """ print('\n\nlooking for stacks with names starting with one of {}'.format(cleaner.describe_prefixes())) stack_list = [] # Construct list of stacks to delete try: stack_paginator = cleaner.cf.get_paginator('list_stacks') # Ignore stacks in DELETE_COMPLETE or DELETE_IN_PROGRESS # Status list: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudformation.html#CloudFormation.Client.list_stacks stack_page_iterator = stack_paginator.paginate( StackStatusFilter=['CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', 'DELETE_FAILED', 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_FAILED', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS', 'IMPORT_IN_PROGRESS', 'IMPORT_COMPLETE', 'IMPORT_ROLLBACK_IN_PROGRESS', 'IMPORT_ROLLBACK_FAILED', 'IMPORT_ROLLBACK_COMPLETE']) for page in stack_page_iterator: summaries = page['StackSummaries'] for stack in summaries: if cleaner.has_prefix(stack['StackName']): print(' found stack {0} with status {1}'.format(stack['StackName'], stack['StackStatus'])) stack_list.append(stack) except KeyError as e: print(" ERROR: Unexpected KeyError while deleting cloud formation stacks. {}".format( exception_utils.message(e))) return except ClientError as e: print(" ERROR: Unexpected error for paginator for the cloud formation client. {}".format( exception_utils.message(e))) return # Delete the stacks for stack in stack_list: stack_id = stack['StackId'] retained_resources = _clean_stack(cleaner, stack_id) print(' deleting stack {}'.format(stack_id)) try: if stack['StackStatus'] == 'DELETE_FAILED': cleaner.cf.delete_stack(StackName=stack_id, RetainResources=retained_resources) else: cleaner.cf.delete_stack(StackName=stack_id) except ClientError as e: print(' ERROR. Failed to delete stack {0} due to {1}'.format(stack_id, exception_utils.message(e))) cleaner.add_to_failed_resources('cloudformation', stack_id) # Wait for the stacks to delete waiter = cleaner.cf.get_waiter('stack_delete_complete') for stack in stack_list: stack_id = stack['StackId'] try: waiter.wait(StackName=stack_id, WaiterConfig={'Delay': cleaner.wait_interval, 'MaxAttempts': cleaner.wait_attempts}) except botocore.exceptions.WaiterError as e: if cleanup_utils.WAITER_ERROR_MESSAGE in exception_utils.message(e): print(" ERROR: Timed out waiting for stack {} to delete".format(stack_id)) else: print(" ERROR: Unexpected error occurred waiting for stack {0} to delete due to {1}".format( stack_id, exception_utils.message(e))) cleaner.add_to_failed_resources('cloudformation', stack_id) print(' Finished deleting stack {}'.format(stack_id)) def _clean_stack(cleaner, stack_id): """ Recursively searches a cloud formation stack for resources that failed to delete. It will then return a list of these resources so that delete_stacks() can be called with the RetainResources param. :param cleaner: A Cleaner object from the main cleanup.py script :param stack_id: The cloud formation stack id to search :return: a list of resource stack id's """ retained_resources = [] print(' getting resources for stack {}'.format(stack_id)) try: response = cleaner.cf.describe_stack_resources(StackName=stack_id) except ClientError as e: print(' ERROR: Error occurred when describe stack resources for {0}. {1}'.format(stack_id, exception_utils.message(e))) return # Don't clean roles from stack, let the stack and IAM clean-up take care of that for resource in response['StackResources']: resource_id = resource.get('PhysicalResourceId', None) if resource_id is not None: if resource['ResourceType'] == 'AWS::CloudFormation::Stack': _clean_stack(cleaner, resource_id) if resource['ResourceStatus'] == 'DELETE_FAILED': retained_resources.append(resource['LogicalResourceId']) return retained_resources