<filename>src/servicenow-stack-set-handler/main.py
"""
Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

This Lambda function creates or updates CloudFormation stackset for provisioning 
AWS Accoutn in ServiceNow. The Lambda function is triggered on receiving Control Tower 
account creation event
"""

import json
import boto3
import botocore
import base64
import traceback
import os
import logging
from time import sleep
from boto3.session import Session
from botocore.exceptions import ClientError
import urllib3
from urllib3.util import parse_url
http = urllib3.PoolManager()

logger = logging.getLogger()
logger.setLevel(logging.INFO)

cloudformation = boto3.client('cloudformation')
s3 = boto3.client('s3')
lambda_client = boto3.client('lambda')
organization_client = boto3.client('organizations')

STACK_SET_ADMIN_ROLE = 'AWSControlTowerStackSetRole'
STACK_SET_EXECUTION_ROLE = 'AWSControlTowerExecution'

SUCCESS = "SUCCESS"
FAILED = "FAILED"

try:
    stack_set_name = os.environ['StackSetName']
    stack_set_regions = os.environ['StackSetRegions'].split(',')
    stack_set_description = os.environ['StackSetDescription']
    stack_template_bucket = os.environ['StackTemplateBucket']
    stack_template_file = os.environ['StackTemplatePrefix']
    servicenow_config_function = os.environ['ServiceNowConfigFunctionName']
    servicenow_role_external_id = os.environ['ServiceNowDiscoveryRoleExternalId']
    enable_servicenow_cloudwatch_intg = os.environ['ServiceNowCloudWatchAlertIntegration']
    servicenow_url = os.environ['ServiceNowEndpointUrl']
    servicenow_creds = os.environ['ServiceNowCreds']
except Exception as e:
    logger.error("Environment variables are missing required fields")
    logger.error(e)
    traceback.print_exc()

def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
    """
    Helper function to return response for CloudFormation custom resource

    :param event:
    :param context
    :param responseStatus
    :param responseData
    :param physicalResourceId
    :param noEcho
    :return:
    """

    responseUrl = event['ResponseURL']

    logger.debug(responseUrl)

    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name
    responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['NoEcho'] = noEcho
    responseBody['Data'] = responseData

    json_responseBody = json.dumps(responseBody)

    logger.debug("Response body:\n" + json_responseBody)

    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }

    try:

        response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers)
        print("Status code: " + response.reason)
    except Exception as e:
        print("send(..) failed executing requests.put(..): " + str(e))

def get_secret(secret_name):
    """
    Retrieves the specified secret from secrets manager in json string format

    :param secret_name: Secret manager secret
    :return: Return secret in json format
    """
    region_name = os.environ['StackSetRegions']

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        if e.response['Error']['Code'] == 'DecryptionFailureException':
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Secrets Manager can't decrypt the protected secret text using the provided KMS key.")
            raise e
        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("An error occurred on the server side.")
            raise e
        elif e.response['Error']['Code'] == 'InvalidParameterException':
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Invalid value for a parameter")
            raise e
        elif e.response['Error']['Code'] == 'InvalidRequestException':
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Provided a parameter value is not valid for the current state of the resource")
            raise e
        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error(f"Unable to find secret with name {secret_name}")
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if 'SecretString' in get_secret_value_response:
            return get_secret_value_response['SecretString']
        else:
            return base64.b64decode(get_secret_value_response['SecretBinary'])

def create_stack_instances(stack_set_name, accounts, regions):
    """
    Creates stack instances for the given stack set in the specified accounts and regions

    :param stack_set_name: Name of CloudFormation StackSet to create/update
    :param stack_set_accounts: Accounts were stackset instances should be created
    :param regions: Regions in which stackset instances should be created
    :return:
    """
    resp = None

    logger.info(f'Creating stack instances for accounts: {accounts}')
    resp = cloudformation.create_stack_instances(
        StackSetName=stack_set_name,
        Accounts=accounts,
        Regions=regions,
        OperationPreferences= {
            'FailureTolerancePercentage': 100,
            'MaxConcurrentPercentage': 100
        }
    )
    logger.debug(f"Create stack instance resp: {json.dumps(resp)}")
    operation_id = resp['OperationId']

    ## wait for stack instance creation to complete
    status = 'RUNNING'
    while status == 'RUNNING' or status == 'STOPPING':
        sleep(30)
        resp = cloudformation.describe_stack_set_operation(
            StackSetName=stack_set_name,
            OperationId=operation_id
        )
        status = resp['StackSetOperation']['Status']
        logger.info(f"Stack Set Operation status: {status}")
    if status == 'FAILED':
        logger.error(f"Stack Set Operation failed. Some of the existing accounts may not be provisioned in ServiceNow")
    else:
        logger.info("Stact Set Operation was successful. Done configuring all existing accounts in ServiceNow")

def create_update_stack_set(stack_set_name, stack_set_accounts):
    """
    Creates stack set with the specified accounts

    :param stack_set_name: Name of CloudFormation StackSet to create/update
    :param stack_set_accounts: Accounts were stackset instances should be created
    :return:
    """
    try:
        logger.info("Retrieving ServiceNow API credentials from Secret Manager")
        creds = json.loads(get_secret(servicenow_creds))
    except Exception as e:
        logger.error(f"Error retrieving servicenow credentials from secret manager")
        raise e
    
    resp = None
    stack_set_exists = True
    
    ## Check if stack set exists
    try:
        logger.info(f"Checking if a Stack Set exists with name: {stack_set_name}")
        resp = cloudformation.describe_stack_set(
            StackSetName=stack_set_name
        )
        logger.debug(f"Describe stack set resp: {json.dumps(resp)}")
    except cloudformation.exceptions.StackSetNotFoundException as e:
        logger.info(f"Can't find StackSet with name {stack_set_name}")
        stack_set_exists = False
    except Exception as e:
        logger.error(f"Error while looking up StackSet with name {stack_set_name}, Error: {str(e)}")
        raise e

    if not stack_set_exists:
        client = boto3.client("sts")
        master_account_id = client.get_caller_identity()["Account"]
        stack_set_params = [
            {
                'ParameterKey': 'pMasterAccountId',
                'ParameterValue': master_account_id
            },
            {
                'ParameterKey': 'pExternalId',
                'ParameterValue': servicenow_role_external_id
            }
        ]
        if enable_servicenow_cloudwatch_intg:
            optional_params = [
                {
                    'ParameterKey': 'pEnableCloudWatchAlarmIntegration',
                    'ParameterValue': 'yes'
                },
                {
                    'ParameterKey': 'pServiceNowUrl',
                    'ParameterValue': parse_url(servicenow_url).hostname
                },
                {
                    'ParameterKey': 'pServiceNowEventUserName',
                    'ParameterValue': creds['username']
                },
                {
                    'ParameterKey': 'pServiceNowEventUserPassword',
                    'ParameterValue': creds['password']
                }
            ]
            stack_set_params.extend(optional_params)
        
        logger.info(f"Creating Stack Set {stack_set_name} ...")
        template_url = "https://{}.s3.amazonaws.com/{}".format(stack_template_bucket, stack_template_file)
        logger.info(template_url)
        try: 
            resp = cloudformation.create_stack_set(
                StackSetName=stack_set_name,
                Description=stack_set_description,
                TemplateURL=template_url,
                Parameters=stack_set_params,
                AdministrationRoleARN="arn:aws:iam::{}:role/service-role/{}".format(master_account_id, STACK_SET_ADMIN_ROLE),
                ExecutionRoleName=STACK_SET_EXECUTION_ROLE,
                Capabilities=[
                    "CAPABILITY_NAMED_IAM"
                ]
            )
        except cloudformation.exceptions.NameAlreadyExistsException as e:
            logger.info(f"StackSet creation failed as another StackSet with that name already exits")
            raise e
        except cloudformation.exceptions.LimitExceededException as e:
            logger.info(f"StackSet creation failed due to execeding cloudformation API limit")
            raise e
        except Exception as e:
            raise e
        logger.info(f"StackSet creation was successfull")
    
    logger.info(f"Creating Stack Instances for Accounts {stack_set_accounts}")
    
    if stack_set_accounts:
        try:
            create_stack_instances(stack_set_name, stack_set_accounts, stack_set_regions)
        except Exception as e:
            logger.error(f"Error creating stack instance, Error {str(e)}")
            return

def delete_stack_set(stack_set_name):
    """
    Deletes stack set and cleans up stack instance

    :param stack_set_name: Name of CloudFormation StackSet to delete
    :return: None
    """
    resp = None
    stack_set_accounts = []
    logger.info(f"Retrieving stack instances for stackset: {stack_set_name}")
    try:
        paginator = cloudformation.get_paginator('list_stack_instances')
        response_iterator = paginator.paginate(
            StackSetName=stack_set_name
        )
        #logger.debug(f"List stack instances response: {json.dumps(response_iterator)}")

        for page in response_iterator:
            stack_resources = page['Summaries']
            for stack_resource in stack_resources:
                print(json.dumps(stack_resource))
                stack_set_accounts.append(stack_resource['Account'])
    except Exception as e:
        logger.error(f"Error listing stack instances, Error: {str(e)}")
        raise e
    
    ## Delete stack instances
    try:
        if stack_set_accounts:
            logger.info(f"Deleting stack instances in accounts: {str(stack_set_accounts)}")
            resp = cloudformation.delete_stack_instances(
                StackSetName=stack_set_name,
                Accounts=stack_set_accounts,
                Regions=stack_set_regions,
                RetainStacks=False
            )
            logger.debug(f"Delete stack instances response: {json.dumps(resp)}")
            operation_id = resp['OperationId']
            # Wait for stack instances to be deleted
            status = 'RUNNING'
            while status == 'RUNNING' or status == 'STOPPING':
                sleep(10)
                resp = cloudformation.describe_stack_set_operation(
                    StackSetName=stack_set_name,
                    OperationId=operation_id
                )
                status = resp['StackSetOperation']['Status']
                logger.debug(f"Stack Set Operation status: {status}")
        
        # Delete the stack set
        logger.info(f"Deleting stack set: {str(stack_set_accounts)}")
        resp = cloudformation.delete_stack_set(
            StackSetName=stack_set_name
        )
        logger.debug(f"Delete stack set response: {json.dumps(resp)}")
    except Exception as e:
        logger.error(f"Eror deleting stack set, Error: {str(e)}")
        raise e

def update_lambda_permissions(function_name, account_id, action):
    """
    Updates permissions of lambda function to allow new accounts access to invoke the lambda function

    :param function_name: Name of the function update permissions
    :param account_id: Account Id to add/update permission for
    :param action: Permission to add
    :return:
    """
    resp = None
    logger.info(f"Updating {function_name} Lambda to allow access for account {account_id}")
    try:
        resp = lambda_client.add_permission(
                FunctionName=function_name,
                StatementId="AccessForAcct-"+account_id,
                Action=action,
                Principal=account_id
            )
        logger.debug(f"Lambda add permissions resp: {json.dumps(resp)}")
    except lambda_client.exceptions.ResourceConflictException:
        logger.info(f"Lambda function policy already has access to account: {account_id} .. skipping")

def retrieve_account_ids_for_organization_unit(ou_id):
    """
    Retrieves active accounts belonging to the specified organizations unit and returns as a list

    :param ou_id: 
    """
    account_ids = []
    paginator = organization_client.get_paginator('list_accounts_for_parent')
    page_iterator = paginator.paginate(
                        ParentId=ou_id
                    )
    for page in page_iterator:
        accounts = page['Accounts']
        for account in accounts:
            if account['Status'] == 'ACTIVE':
                account_ids.append(account['Id'])

    paginator = organization_client.get_paginator('list_children')
    page_iterator = paginator.paginate(
                        ParentId=ou_id,
                        ChildType='ORGANIZATIONAL_UNIT'
                    )
    for page in page_iterator:
        ous = page['Children']
        for ou in ous:
            account_ids = account_ids + retrieve_account_ids_for_organization_unit(ou['Id'])

    return account_ids

def retrieve_all_accounts_from_organization(master_account):
    """
    Retrieves all active accounts under the organizations (except parent account) and returns as a list

    :param master_account: Acount Id for master account
    """
    paginator = organization_client.get_paginator('list_accounts')
    page_iterator = paginator.paginate()
    account_ids = []
    for page in page_iterator:
        accounts = page['Accounts']
        for account in accounts:
            if account['Id'] != master_account and account['Status'] == 'ACTIVE':
                account_ids.append(account['Id'])
    return account_ids


def lambda_handler(event, context):
    """
    The Lambda function handler to process Control tower life cycle event and create/update stackset for ServiceNow
    for ServiceNow setup. 
   
    :param event: The event passed by Lambda
    :param context: The context passed by Lambda
    """

    logger.info(json.dumps(event))
    response_data = {}
    stack_set_accounts = []
    custom_resource_invocation = False
    ## Check if we got custom resource event for provisioning existing account or new account event
    if "RequestType" in event:
        request_type = event['RequestType']
        params = event['ResourceProperties']
        custom_resource_invocation = True
        configure_existing_accounts = params['ConfigureExistingAccounts']
        if configure_existing_accounts == 'true':
            ous_to_configure = params['OrganizationUnitsToConfigure']
            if 'ou-' in ous_to_configure:
                ou_list = ous_to_configure.split(',')
            else:
                ou_list = []
            ## retrieve current account
            sts = boto3.client('sts')
            resp = sts.get_caller_identity()
            current_account = resp['Account']
            if request_type != 'Delete':
                logger.info("Retrieving existing accounts from Organizations")
                if ou_list:
                    for ou in ou_list:
                        accounts = retrieve_account_ids_for_organization_unit(ou)
                        stack_set_accounts = stack_set_accounts + accounts
                else:
                    stack_set_accounts = retrieve_all_accounts_from_organization(current_account)
            else:
                try:
                    logger.info(f"Deleting stackset: {stack_set_name}")
                    delete_stack_set(stack_set_name)
                    send(event, context, SUCCESS, response_data, "ConfigureExistingAccounts")
                    return
                except Exception as e:
                    response_data['ERROR'] = f"StackSet deletion failed, Error {str(e)}"
                    send(event, context, FAILED, response_data, "ConfigureExistingAccounts")
                    return
    else:
        # Process new Account creation event
        try:
            ct_event_detail = event['detail']['serviceEventDetails']
            account_creation_state = ct_event_detail['createManagedAccountStatus']['state']
            if account_creation_state != "SUCCEEDED":
                logger.info(f"Control tower could not successfully create the account.. skipping event")
                return
            account = ct_event_detail['createManagedAccountStatus']['account']
            # account_info = {}
            # account_info['Id'] = account['accountId']
            # account_info['Name'] = account['accountName']
            stack_set_accounts.append(account['accountId'])
        except Exception as e:
            logger.error(f'Input event is missing required fields')
            logger.error(str(e))
            traceback.print_exc()
            return

    ## update ServiceNow config lambda permissions.
    ## TODO Remove this when Lambda Function supports sharing using aws:PrincipalOrgId in resource policy
    for account_id in stack_set_accounts:
        update_lambda_permissions(servicenow_config_function, account_id, "lambda:InvokeFunction")

    try:
        create_update_stack_set(stack_set_name, stack_set_accounts)
    except Exception as e:
        logger.error(f"StackSet creation failed, Error {str(e)}")
        if custom_resource_invocation:
            response_data['ERROR'] = f"StackSet creation failed, Error {str(e)}"
            send(event, context, FAILED, response_data, "ConfigureExistingAccounts")
            return

    if custom_resource_invocation:
        send(event, context, SUCCESS, response_data, "ConfigureExistingAccounts")
        return
<filename>src/servicenow-stack-set-handler/main.py
"""
Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

This Lambda function creates or updates CloudFormation stackset for provisioning 
AWS Accoutn in ServiceNow. The Lambda function is triggered on receiving Control Tower 
account creation event
"""

import json
import boto3
import botocore
import base64
import traceback
import os
import logging
from time import sleep
from boto3.session import Session
from botocore.exceptions import ClientError
import urllib3
from urllib3.util import parse_url
http = urllib3.PoolManager()

logger = logging.getLogger()
logger.setLevel(logging.INFO)

cloudformation = boto3.client('cloudformation')
s3 = boto3.client('s3')
lambda_client = boto3.client('lambda')
organization_client = boto3.client('organizations')

STACK_SET_ADMIN_ROLE = 'AWSControlTowerStackSetRole'
STACK_SET_EXECUTION_ROLE = 'AWSControlTowerExecution'

SUCCESS = "SUCCESS"
FAILED = "FAILED"

try:
    stack_set_name = os.environ['StackSetName']
    stack_set_regions = os.environ['StackSetRegions'].split(',')
    stack_set_description = os.environ['StackSetDescription']
    stack_template_bucket = os.environ['StackTemplateBucket']
    stack_template_file = os.environ['StackTemplatePrefix']
    servicenow_config_function = os.environ['ServiceNowConfigFunctionName']
    servicenow_role_external_id = os.environ['ServiceNowDiscoveryRoleExternalId']
    enable_servicenow_cloudwatch_intg = os.environ['ServiceNowCloudWatchAlertIntegration']
    servicenow_url = os.environ['ServiceNowEndpointUrl']
    servicenow_creds = os.environ['ServiceNowCreds']
except Exception as e:
    logger.error("Environment variables are missing required fields")
    logger.error(e)
    traceback.print_exc()

def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
    """
    Helper function to return response for CloudFormation custom resource

    :param event:
    :param context
    :param responseStatus
    :param responseData
    :param physicalResourceId
    :param noEcho
    :return:
    """

    responseUrl = event['ResponseURL']

    logger.debug(responseUrl)

    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name
    responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['NoEcho'] = noEcho
    responseBody['Data'] = responseData

    json_responseBody = json.dumps(responseBody)

    logger.debug("Response body:\n" + json_responseBody)

    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }

    try:

        response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers)
        print("Status code: " + response.reason)
    except Exception as e:
        print("send(..) failed executing requests.put(..): " + str(e))

def get_secret(secret_name):
    """
    Retrieves the specified secret from secrets manager in json string format

    :param secret_name: Secret manager secret
    :return: Return secret in json format
    """
    region_name = os.environ['StackSetRegions']

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        if e.response['Error']['Code'] == 'DecryptionFailureException':
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Secrets Manager can't decrypt the protected secret text using the provided KMS key.")
            raise e
        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("An error occurred on the server side.")
            raise e
        elif e.response['Error']['Code'] == 'InvalidParameterException':
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Invalid value for a parameter")
            raise e
        elif e.response['Error']['Code'] == 'InvalidRequestException':
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error("Provided a parameter value is not valid for the current state of the resource")
            raise e
        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            logger.error(f"Unable to find secret with name {secret_name}")
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if 'SecretString' in get_secret_value_response:
            return get_secret_value_response['SecretString']
        else:
            return base64.b64decode(get_secret_value_response['SecretBinary'])

def create_stack_instances(stack_set_name, accounts, regions):
    """
    Creates stack instances for the given stack set in the specified accounts and regions

    :param stack_set_name: Name of CloudFormation StackSet to create/update
    :param stack_set_accounts: Accounts were stackset instances should be created
    :param regions: Regions in which stackset instances should be created
    :return:
    """
    resp = None

    logger.info(f'Creating stack instances for accounts: {accounts}')
    resp = cloudformation.create_stack_instances(
        StackSetName=stack_set_name,
        Accounts=accounts,
        Regions=regions,
        OperationPreferences= {
            'FailureTolerancePercentage': 100,
            'MaxConcurrentPercentage': 100
        }
    )
    logger.debug(f"Create stack instance resp: {json.dumps(resp)}")
    operation_id = resp['OperationId']

    ## wait for stack instance creation to complete
    status = 'RUNNING'
    while status == 'RUNNING' or status == 'STOPPING':
        sleep(30)
        resp = cloudformation.describe_stack_set_operation(
            StackSetName=stack_set_name,
            OperationId=operation_id
        )
        status = resp['StackSetOperation']['Status']
        logger.info(f"Stack Set Operation status: {status}")
    if status == 'FAILED':
        logger.error(f"Stack Set Operation failed. Some of the existing accounts may not be provisioned in ServiceNow")
    else:
        logger.info("Stact Set Operation was successful. Done configuring all existing accounts in ServiceNow")

def create_update_stack_set(stack_set_name, stack_set_accounts):
    """
    Creates stack set with the specified accounts

    :param stack_set_name: Name of CloudFormation StackSet to create/update
    :param stack_set_accounts: Accounts were stackset instances should be created
    :return:
    """
    try:
        logger.info("Retrieving ServiceNow API credentials from Secret Manager")
        creds = json.loads(get_secret(servicenow_creds))
    except Exception as e:
        logger.error(f"Error retrieving servicenow credentials from secret manager")
        raise e
    
    resp = None
    stack_set_exists = True
    
    ## Check if stack set exists
    try:
        logger.info(f"Checking if a Stack Set exists with name: {stack_set_name}")
        resp = cloudformation.describe_stack_set(
            StackSetName=stack_set_name
        )
        logger.debug(f"Describe stack set resp: {json.dumps(resp)}")
    except cloudformation.exceptions.StackSetNotFoundException as e:
        logger.info(f"Can't find StackSet with name {stack_set_name}")
        stack_set_exists = False
    except Exception as e:
        logger.error(f"Error while looking up StackSet with name {stack_set_name}, Error: {str(e)}")
        raise e

    if not stack_set_exists:
        client = boto3.client("sts")
        master_account_id = client.get_caller_identity()["Account"]
        stack_set_params = [
            {
                'ParameterKey': 'pMasterAccountId',
                'ParameterValue': master_account_id
            },
            {
                'ParameterKey': 'pExternalId',
                'ParameterValue': servicenow_role_external_id
            }
        ]
        if enable_servicenow_cloudwatch_intg:
            optional_params = [
                {
                    'ParameterKey': 'pEnableCloudWatchAlarmIntegration',
                    'ParameterValue': 'yes'
                },
                {
                    'ParameterKey': 'pServiceNowUrl',
                    'ParameterValue': parse_url(servicenow_url).hostname
                },
                {
                    'ParameterKey': 'pServiceNowEventUserName',
                    'ParameterValue': creds['username']
                },
                {
                    'ParameterKey': 'pServiceNowEventUserPassword',
                    'ParameterValue': creds['password']
                }
            ]
            stack_set_params.extend(optional_params)
        
        logger.info(f"Creating Stack Set {stack_set_name} ...")
        template_url = "https://{}.s3.amazonaws.com/{}".format(stack_template_bucket, stack_template_file)
        logger.info(template_url)
        try: 
            resp = cloudformation.create_stack_set(
                StackSetName=stack_set_name,
                Description=stack_set_description,
                TemplateURL=template_url,
                Parameters=stack_set_params,
                AdministrationRoleARN="arn:aws:iam::{}:role/service-role/{}".format(master_account_id, STACK_SET_ADMIN_ROLE),
                ExecutionRoleName=STACK_SET_EXECUTION_ROLE,
                Capabilities=[
                    "CAPABILITY_NAMED_IAM"
                ]
            )
        except cloudformation.exceptions.NameAlreadyExistsException as e:
            logger.info(f"StackSet creation failed as another StackSet with that name already exits")
            raise e
        except cloudformation.exceptions.LimitExceededException as e:
            logger.info(f"StackSet creation failed due to execeding cloudformation API limit")
            raise e
        except Exception as e:
            raise e
        logger.info(f"StackSet creation was successfull")
    
    logger.info(f"Creating Stack Instances for Accounts {stack_set_accounts}")
    
    if stack_set_accounts:
        try:
            create_stack_instances(stack_set_name, stack_set_accounts, stack_set_regions)
        except Exception as e:
            logger.error(f"Error creating stack instance, Error {str(e)}")
            return

def delete_stack_set(stack_set_name):
    """
    Deletes stack set and cleans up stack instance

    :param stack_set_name: Name of CloudFormation StackSet to delete
    :return: None
    """
    resp = None
    stack_set_accounts = []
    logger.info(f"Retrieving stack instances for stackset: {stack_set_name}")
    try:
        paginator = cloudformation.get_paginator('list_stack_instances')
        response_iterator = paginator.paginate(
            StackSetName=stack_set_name
        )
        #logger.debug(f"List stack instances response: {json.dumps(response_iterator)}")

        for page in response_iterator:
            stack_resources = page['Summaries']
            for stack_resource in stack_resources:
                print(json.dumps(stack_resource))
                stack_set_accounts.append(stack_resource['Account'])
    except Exception as e:
        logger.error(f"Error listing stack instances, Error: {str(e)}")
        raise e
    
    ## Delete stack instances
    try:
        if stack_set_accounts:
            logger.info(f"Deleting stack instances in accounts: {str(stack_set_accounts)}")
            resp = cloudformation.delete_stack_instances(
                StackSetName=stack_set_name,
                Accounts=stack_set_accounts,
                Regions=stack_set_regions,
                RetainStacks=False
            )
            logger.debug(f"Delete stack instances response: {json.dumps(resp)}")
            operation_id = resp['OperationId']
            # Wait for stack instances to be deleted
            status = 'RUNNING'
            while status == 'RUNNING' or status == 'STOPPING':
                sleep(10)
                resp = cloudformation.describe_stack_set_operation(
                    StackSetName=stack_set_name,
                    OperationId=operation_id
                )
                status = resp['StackSetOperation']['Status']
                logger.debug(f"Stack Set Operation status: {status}")
        
        # Delete the stack set
        logger.info(f"Deleting stack set: {str(stack_set_accounts)}")
        resp = cloudformation.delete_stack_set(
            StackSetName=stack_set_name
        )
        logger.debug(f"Delete stack set response: {json.dumps(resp)}")
    except Exception as e:
        logger.error(f"Eror deleting stack set, Error: {str(e)}")
        raise e

def update_lambda_permissions(function_name, account_id, action):
    """
    Updates permissions of lambda function to allow new accounts access to invoke the lambda function

    :param function_name: Name of the function update permissions
    :param account_id: Account Id to add/update permission for
    :param action: Permission to add
    :return:
    """
    resp = None
    logger.info(f"Updating {function_name} Lambda to allow access for account {account_id}")
    try:
        resp = lambda_client.add_permission(
                FunctionName=function_name,
                StatementId="AccessForAcct-"+account_id,
                Action=action,
                Principal=account_id
            )
        logger.debug(f"Lambda add permissions resp: {json.dumps(resp)}")
    except lambda_client.exceptions.ResourceConflictException:
        logger.info(f"Lambda function policy already has access to account: {account_id} .. skipping")

def retrieve_account_ids_for_organization_unit(ou_id):
    """
    Retrieves active accounts belonging to the specified organizations unit and returns as a list

    :param ou_id: 
    """
    account_ids = []
    paginator = organization_client.get_paginator('list_accounts_for_parent')
    page_iterator = paginator.paginate(
                        ParentId=ou_id
                    )
    for page in page_iterator:
        accounts = page['Accounts']
        for account in accounts:
            if account['Status'] == 'ACTIVE':
                account_ids.append(account['Id'])

    paginator = organization_client.get_paginator('list_children')
    page_iterator = paginator.paginate(
                        ParentId=ou_id,
                        ChildType='ORGANIZATIONAL_UNIT'
                    )
    for page in page_iterator:
        ous = page['Children']
        for ou in ous:
            account_ids = account_ids + retrieve_account_ids_for_organization_unit(ou['Id'])

    return account_ids

def retrieve_all_accounts_from_organization(master_account):
    """
    Retrieves all active accounts under the organizations (except parent account) and returns as a list

    :param master_account: Acount Id for master account
    """
    paginator = organization_client.get_paginator('list_accounts')
    page_iterator = paginator.paginate()
    account_ids = []
    for page in page_iterator:
        accounts = page['Accounts']
        for account in accounts:
            if account['Id'] != master_account and account['Status'] == 'ACTIVE':
                account_ids.append(account['Id'])
    return account_ids


def lambda_handler(event, context):
    """
    The Lambda function handler to process Control tower life cycle event and create/update stackset for ServiceNow
    for ServiceNow setup. 
   
    :param event: The event passed by Lambda
    :param context: The context passed by Lambda
    """

    logger.info(json.dumps(event))
    response_data = {}
    stack_set_accounts = []
    custom_resource_invocation = False
    ## Check if we got custom resource event for provisioning existing account or new account event
    if "RequestType" in event:
        request_type = event['RequestType']
        params = event['ResourceProperties']
        custom_resource_invocation = True
        configure_existing_accounts = params['ConfigureExistingAccounts']
        if configure_existing_accounts == 'true':
            ous_to_configure = params['OrganizationUnitsToConfigure']
            if 'ou-' in ous_to_configure:
                ou_list = ous_to_configure.split(',')
            else:
                ou_list = []
            ## retrieve current account
            sts = boto3.client('sts')
            resp = sts.get_caller_identity()
            current_account = resp['Account']
            if request_type != 'Delete':
                logger.info("Retrieving existing accounts from Organizations")
                if ou_list:
                    for ou in ou_list:
                        accounts = retrieve_account_ids_for_organization_unit(ou)
                        stack_set_accounts = stack_set_accounts + accounts
                else:
                    stack_set_accounts = retrieve_all_accounts_from_organization(current_account)
            else:
                try:
                    logger.info(f"Deleting stackset: {stack_set_name}")
                    delete_stack_set(stack_set_name)
                    send(event, context, SUCCESS, response_data, "ConfigureExistingAccounts")
                    return
                except Exception as e:
                    response_data['ERROR'] = f"StackSet deletion failed, Error {str(e)}"
                    send(event, context, FAILED, response_data, "ConfigureExistingAccounts")
                    return
    else:
        # Process new Account creation event
        try:
            ct_event_detail = event['detail']['serviceEventDetails']
            account_creation_state = ct_event_detail['createManagedAccountStatus']['state']
            if account_creation_state != "SUCCEEDED":
                logger.info(f"Control tower could not successfully create the account.. skipping event")
                return
            account = ct_event_detail['createManagedAccountStatus']['account']
            # account_info = {}
            # account_info['Id'] = account['accountId']
            # account_info['Name'] = account['accountName']
            stack_set_accounts.append(account['accountId'])
        except Exception as e:
            logger.error(f'Input event is missing required fields')
            logger.error(str(e))
            traceback.print_exc()
            return

    ## update ServiceNow config lambda permissions.
    ## TODO Remove this when Lambda Function supports sharing using aws:PrincipalOrgId in resource policy
    for account_id in stack_set_accounts:
        update_lambda_permissions(servicenow_config_function, account_id, "lambda:InvokeFunction")

    try:
        create_update_stack_set(stack_set_name, stack_set_accounts)
    except Exception as e:
        logger.error(f"StackSet creation failed, Error {str(e)}")
        if custom_resource_invocation:
            response_data['ERROR'] = f"StackSet creation failed, Error {str(e)}"
            send(event, context, FAILED, response_data, "ConfigureExistingAccounts")
            return

    if custom_resource_invocation:
        send(event, context, SUCCESS, response_data, "ConfigureExistingAccounts")
        return
Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  This Lambda function creates or updates CloudFormation stackset for provisioning  AWS Accoutn in ServiceNow. The Lambda function is triggered on receiving Control Tower  account creation event Helper function to return response for CloudFormation custom resource      :param event:     :param context     :param responseStatus     :param responseData     :param physicalResourceId     :param noEcho     :return: Retrieves the specified secret from secrets manager in json string format      :param secret_name: Secret manager secret     :return: Return secret in json format # Create a Secrets Manager client # Secrets Manager can't decrypt the protected secret text using the provided KMS key. # Deal with the exception here, and/or rethrow at your discretion. # An error occurred on the server side. # Deal with the exception here, and/or rethrow at your discretion. # You provided an invalid value for a parameter. # Deal with the exception here, and/or rethrow at your discretion. # You provided a parameter value that is not valid for the current state of the resource. # Deal with the exception here, and/or rethrow at your discretion. # We can't find the resource that you asked for. # Deal with the exception here, and/or rethrow at your discretion. # Decrypts secret using the associated KMS CMK. # Depending on whether the secret is a string or binary, one of these fields will be populated. Creates stack instances for the given stack set in the specified accounts and regions      :param stack_set_name: Name of CloudFormation StackSet to create/update     :param stack_set_accounts: Accounts were stackset instances should be created     :param regions: Regions in which stackset instances should be created     :return: ## wait for stack instance creation to complete Creates stack set with the specified accounts      :param stack_set_name: Name of CloudFormation StackSet to create/update     :param stack_set_accounts: Accounts were stackset instances should be created     :return: ## Check if stack set exists Deletes stack set and cleans up stack instance      :param stack_set_name: Name of CloudFormation StackSet to delete     :return: None #logger.debug(f"List stack instances response: {json.dumps(response_iterator)}") ## Delete stack instances # Wait for stack instances to be deleted # Delete the stack set Updates permissions of lambda function to allow new accounts access to invoke the lambda function      :param function_name: Name of the function update permissions     :param account_id: Account Id to add/update permission for     :param action: Permission to add     :return: Retrieves active accounts belonging to the specified organizations unit and returns as a list      :param ou_id: Retrieves all active accounts under the organizations (except parent account) and returns as a list      :param master_account: Acount Id for master account The Lambda function handler to process Control tower life cycle event and create/update stackset for ServiceNow     for ServiceNow setup.          :param event: The event passed by Lambda     :param context: The context passed by Lambda ## Check if we got custom resource event for provisioning existing account or new account event ## retrieve current account # Process new Account creation event # account_info = {} # account_info['Id'] = account['accountId'] # account_info['Name'] = account['accountName'] ## update ServiceNow config lambda permissions. ## TODO Remove this when Lambda Function supports sharing using aws:PrincipalOrgId in resource policy