def wait_stack_states(success_states, failure_states, lambda_payload,
                      lambda_context):
    """
    Wait for stack states, either be it success or failure. If none of the states
    appear and lambda is running out of time, it will be re-invoked with lambda_payload
    parameters
    :param lambda_context:
    :param stack_id:
    :param success_states:
    :param failure_states:
    :param lambda_payload:
    :return:
    """
    manage = stack_manage.StackManagement()
    result = manage.wait_stack_status(
        lambda_payload['ResourceProperties']['Region'],
        lambda_payload['PhysicalResourceId'], success_states, failure_states,
        lambda_context)

    # in this case we need to restart lambda execution
    if result is None:
        invoke = lambda_invoker.LambdaInvoker()
        invoke.invoke(lambda_payload)
    else:
        # one of the states is reached, and reply should be sent back to cloud formation
        cfn_response = cr_response.CustomResourceResponse(lambda_payload)
        cfn_response.response['PhysicalResourceId'] = lambda_payload[
            'PhysicalResourceId']
        cfn_response.response['Status'] = result.status
        cfn_response.response['Reason'] = result.reason
        cfn_response.response['StackId'] = lambda_payload['StackId']
        cfn_response.respond()
def create_update_stack(cmd, payload):
    if 'Capabilities' not in payload['ResourceProperties']:
        payload['ResourceProperties']['Capabilities'] = 'CAPABILITY_IAM'

    # compile stack parameters
    stack_params = {}
    for key, value in payload['ResourceProperties'].items():
        if key.startswith('StackParam_'):
            param_key = key.replace('StackParam_', '')
            param_value = value
            stack_params[param_key] = param_value

    # instantiate and use management handler
    manage = stack_manage.StackManagement()

    on_failure = 'DELETE'
    if 'OnFailure' in payload['ResourceProperties']:
        on_failure = payload['ResourceProperties']['OnFailure']

    stack_id = ''
    if cmd == 'create':
        stack_id = manage.create(
            payload['ResourceProperties']['Region'],
            payload['ResourceProperties']['StackName'],
            payload['ResourceProperties']['TemplateUrl'], stack_params,
            payload['ResourceProperties']['Capabilities'].split(','),
            on_failure)
    elif cmd == 'update':
        stack_id = payload['PhysicalResourceId']
        result = manage.update(
            payload['ResourceProperties']['Region'],
            stack_id,
            payload['ResourceProperties']['TemplateUrl'],
            stack_params,
            payload['ResourceProperties']['Capabilities'].split(','),
        )
        # no updates to be performed
        if result is None:
            cfn_response = cr_response.CustomResourceResponse(payload)
            cfn_response.respond()
            return None
    else:
        raise 'Cmd must be create or update'

    return stack_id
def delete_stack(payload):
    manage = stack_manage.StackManagement()
    region = payload['ResourceProperties']['Region']
    stack_id = payload['PhysicalResourceId']
    manage.delete(region, payload['ResourceProperties']['StackName'])
    return stack_id
def lambda_handler(payload, context):
    # if lambda invoked to wait for stack status
    print(f"Received event:{json.dumps(payload)}")

    # handle disable region situation
    if 'EnabledRegions' in payload['ResourceProperties']:
        region_list = payload['ResourceProperties']['EnabledRegions'].split(
            ',')
        current_region = payload['ResourceProperties']['Region']
        print(
            f"EnabledRegions: {region_list}. Current region={current_region}")

        if current_region not in region_list:
            # if this is create request just skip
            if payload['RequestType'] == 'Create' or payload[
                    'RequestType'] == 'Update':
                print(f"{current_region} not enabled, skipping")
                # report disabled
                # in case of region disable (update), physical record changes, so cleanup delete request is
                # sent subsequently via Cf, which will delete the stack
                respond_disabled_region(current_region, payload)
                return

    # lambda was invoked by itself, we just have to wait for stack operation to be completed
    if ('WaitComplete' in payload) and (payload['WaitComplete']):
        print("Waiting for stack status...")
        if payload['RequestType'] == 'Create':
            wait_stack_states(create_stack_success_states,
                              create_stack_failure_states, payload, context)

        elif payload['RequestType'] == 'Update':
            wait_stack_states(update_stack_success_states,
                              update_stack_failure_states, payload, context)

        elif payload['RequestType'] == 'Delete':
            wait_stack_states(delete_stack_success_states,
                              delete_stack_failure_states, payload, context)

    # lambda was invoked directly by cf
    else:
        # depending on request type different handler is called
        print("Executing stack CRUD...")
        stack_id = None
        if 'PhysicalResourceId' in payload:
            stack_id = payload['PhysicalResourceId']
        try:
            manage = stack_manage.StackManagement()
            stack_name = payload['ResourceProperties']['StackName']
            region = payload['ResourceProperties']['Region']
            stack_exists = manage.stack_exists(region, stack_name)

            if payload['RequestType'] == 'Create':
                # stack exists, create request => update
                # stack not exists, create request => create
                if stack_exists:
                    print(
                        f"Create request came for {stack_name}, but it already exists in {region}, updating..."
                    )
                    payload['RequestType'] = 'Update'
                    lambda_handler(payload, context)
                    return
                else:
                    stack_id = create_update_stack('create', payload)

            elif payload['RequestType'] == 'Update':
                # stack exists, update request => update
                # stack not exists, update request => create
                if stack_exists:
                    stack_id = create_update_stack('update', payload)
                    if stack_id is None:
                        # no updates to be performed
                        return
                else:
                    print(
                        f"Update request came for {stack_name}, but it does not exist in {region}, creating..."
                    )
                    payload['RequestType'] = 'Create'
                    lambda_handler(payload, context)
                    return

            elif payload['RequestType'] == 'Delete':
                # stack exists, delete request => delete
                # stack not exists, delete request => report ok
                # for delete we are interested in actual stack id
                stack_exists = manage.stack_exists(region, stack_id)
                if stack_exists:
                    delete_stack(payload)
                else:
                    # reply with success
                    print(
                        f"Delete request came for {stack_name}, but it is nowhere to be found..."
                    )
                    cfn_response = cr_response.CustomResourceResponse(payload)
                    cfn_response.response[
                        'Reason'] = 'CloudFormation stack has not been found, may be removed manually'
                    cfn_response.respond()
                    return

            # if haven't moved to other operation, set payloads stack id to created/updated stack and wait
            # for appropriate stack status
            payload['PhysicalResourceId'] = stack_id
            payload['WaitComplete'] = True
            invoker = lambda_invoker.LambdaInvoker()
            invoker.invoke(payload)

        except Exception as e:
            print(f"Exception:{e}\n{str(e)}")
            print(traceback.format_exc())
            cfn_response = cr_response.CustomResourceResponse(payload)
            if 'PhysicalResourceId' in payload:
                cfn_response.response['PhysicalResourceId'] = payload[
                    'PhysicalResourceId']
            cfn_response.response['Status'] = 'FAILED'
            cfn_response.response['Reason'] = str(e)
            cfn_response.respond()
            raise e