class StateMachineTriggerLambda(object):
    def __init__(self, logger, sm_arns_map, staging_bucket, manifest_file_path,
                 pipeline_stage, token, execution_mode, primary_account_id):
        self.state_machine = StateMachine(logger)
        self.ssm = SSM(logger)
        self.s3 = S3(logger)
        self.send = Metrics(logger)
        self.param_handler = ParamsHandler(logger)
        self.logger = logger
        self.sm_arns_map = sm_arns_map
        self.manifest = None
        self.staging_bucket = staging_bucket
        self.manifest_file_path = manifest_file_path
        self.token = token
        self.pipeline_stage = pipeline_stage
        self.manifest_folder = manifest_file_path[:-len(MANIFEST_FILE_NAME)]
        if execution_mode.lower() == 'sequential':
            self.isSequential = True
        else:
            self.isSequential = False
        self.index = 100
        self.primary_account_id = primary_account_id

    def _save_sm_exec_arn(self, list_sm_exec_arns):
        if list_sm_exec_arns is not None and type(list_sm_exec_arns) is list:
            self.logger.debug(
                "Saving the token:{} with list of sm_exec_arns:{}".format(
                    self.token, list_sm_exec_arns))
            if len(list_sm_exec_arns) > 0:
                sm_exec_arns = ",".join(
                    list_sm_exec_arns
                )  # Create comma seperated string from list e.g. 'a','b','c'
                self.ssm.put_parameter(
                    self.token,
                    sm_exec_arns)  # Store the list of SM execution ARNs in SSM
            else:
                self.ssm.put_parameter(self.token, 'PASS')
        else:
            raise Exception(
                "Expecting a list of state machine execution ARNs to store in SSM for token:{}, but found nothing to store."
                .format(self.token))

    def _stage_template(self, relative_template_path):
        if relative_template_path.lower().startswith('s3'):
            # Convert the remote template URL s3://bucket-name/object
            # to Virtual-hosted style URL https://bucket-name.s3.amazonaws.com/object
            t = relative_template_path.split("/", 3)
            s3_url = "https://{}.s3.amazonaws.com/{}".format(t[2], t[3])
        else:
            local_file = os.path.join(self.manifest_folder,
                                      relative_template_path)
            remote_file = "{}/{}_{}".format(
                TEMPLATE_KEY_PREFIX, self.token,
                relative_template_path[relative_template_path.rfind('/') + 1:])
            logger.info(
                "Uploading the template file: {} to S3 bucket: {} and key: {}".
                format(local_file, self.staging_bucket, remote_file))
            self.s3.upload_file(self.staging_bucket, local_file, remote_file)
            s3_url = "{}{}{}{}".format('https://s3.amazonaws.com/',
                                       self.staging_bucket, '/', remote_file)
        return s3_url

    def _download_remote_file(self, remote_s3_path):
        _file = tempfile.mkstemp()[1]
        t = remote_s3_path.split("/", 3)  # s3://bucket-name/key
        remote_bucket = t[2]  # Bucket name
        remote_key = t[3]  # Key
        logger.info("Downloading {}/{} from S3 to {}".format(
            remote_bucket, remote_key, _file))
        self.s3.download_file(remote_bucket, remote_key, _file)
        return _file

    def _load_policy(self, relative_policy_path):
        if relative_policy_path.lower().startswith('s3'):
            policy_file = self._download_remote_file(relative_policy_path)
        else:
            policy_file = os.path.join(self.manifest_folder,
                                       relative_policy_path)

        logger.info("Parsing the policy file: {}".format(policy_file))

        with open(policy_file, 'r') as content_file:
            policy_file_content = content_file.read()

        #Check if valid json
        json.loads(policy_file_content)
        #Return the Escaped JSON text
        return policy_file_content.replace('"', '\"').replace('\n', '\r\n')

    def _load_params(self, relative_parameter_path, account=None, region=None):
        if relative_parameter_path.lower().startswith('s3'):
            parameter_file = self._download_remote_file(
                relative_parameter_path)
        else:
            parameter_file = os.path.join(self.manifest_folder,
                                          relative_parameter_path)

        logger.info("Parsing the parameter file: {}".format(parameter_file))

        with open(parameter_file, 'r') as content_file:
            parameter_file_content = content_file.read()

        params = json.loads(parameter_file_content)
        if account is not None:
            #Deploying Core resource Stack Set
            # The last parameter is set to False, because we do not want to replace the SSM parameter values yet.
            sm_params = self.param_handler.update_params(
                params, account, region, False)
        else:
            # Deploying Baseline resource Stack Set
            sm_params = self.param_handler.update_params(params)

        logger.info("Input Parameters for State Machine: {}".format(sm_params))
        return sm_params

    def _load_template_rules(self, relative_rules_path):
        rules_file = os.path.join(self.manifest_folder, relative_rules_path)
        logger.info("Parsing the template rules file: {}".format(rules_file))

        with open(rules_file, 'r') as content_file:
            rules_file_content = content_file.read()

        rules = json.loads(rules_file_content)

        logger.info(
            "Template Constraint Rules for State Machine: {}".format(rules))

        return rules

    def _populate_ssm_params(self, sm_input):
        # The scenario is if you have one core resource that exports output from CFN stack to SSM parameter
        # and then the next core resource reads the SSM parameter as input, then it has to wait for the first core resource to
        # finish; read the SSM parameters and use its value as input for second core resource's input for SM
        # Get the parameters for CFN template from sm_input
        logger.debug("Populating SSM parameter values for SM input: {}".format(
            sm_input))
        params = sm_input.get('ResourceProperties').get('Parameters', {})
        # First transform it from {name: value} to [{'ParameterKey': name}, {'ParameterValue': value}]
        # then replace the SSM parameter names with its values
        sm_params = self.param_handler.update_params(transform_params(params))
        # Put it back into the sm_input
        sm_input.get('ResourceProperties').update({'Parameters': sm_params})
        logger.debug(
            "Done populating SSM parameter values for SM input: {}".format(
                sm_input))
        return sm_input

    def _create_ssm_input_map(self, ssm_parameters):
        ssm_input_map = {}

        for ssm_parameter in ssm_parameters:
            key = ssm_parameter.name
            value = ssm_parameter.value
            ssm_value = self.param_handler.update_params(
                transform_params({key: value}))
            ssm_input_map.update(ssm_value)

        return ssm_input_map

    def _create_state_machine_input_map(self,
                                        input_params,
                                        request_type='Create'):
        request = {}
        request.update({'RequestType': request_type})
        request.update({'ResourceProperties': input_params})

        return request

    def _create_account_state_machine_input_map(self,
                                                ou_name,
                                                account_name='',
                                                account_email='',
                                                ssm_map=None):
        input_params = {}
        input_params.update({'OUName': ou_name})
        input_params.update({'AccountName': account_name})
        input_params.update({'AccountEmail': account_email})
        if ssm_map is not None:
            input_params.update({'SSMParameters': ssm_map})
        return self._create_state_machine_input_map(input_params)

    def _create_stack_set_state_machine_input_map(
            self,
            stack_set_name,
            template_url,
            parameters,
            account_list=[],
            regions_list=[],
            ssm_map=None,
            capabilities='CAPABILITY_NAMED_IAM'):
        input_params = {}
        input_params.update({'StackSetName': sanitize(stack_set_name)})
        input_params.update({'TemplateURL': template_url})
        input_params.update({'Parameters': parameters})
        input_params.update({'Capabilities': capabilities})

        if len(account_list) > 0:
            input_params.update({'AccountList': account_list})
            if len(regions_list) > 0:
                input_params.update({'RegionList': regions_list})
            else:
                input_params.update({'RegionList': [self.manifest.region]})
        else:
            input_params.update({'AccountList': ''})
            input_params.update({'RegionList': ''})

        if ssm_map is not None:
            input_params.update({'SSMParameters': ssm_map})

        return self._create_state_machine_input_map(input_params)

    def _create_service_control_policy_state_machine_input_map(
            self, policy_name, policy_content, policy_desc=''):
        input_params = {}
        policy_doc = {}
        policy_doc.update({'Name': sanitize(policy_name)})
        policy_doc.update({'Description': policy_desc})
        policy_doc.update({'Content': policy_content})
        input_params.update({'PolicyDocument': policy_doc})
        input_params.update({'AccountId': ''})
        input_params.update({'PolicyList': []})
        input_params.update({'Operation': ''})
        return self._create_state_machine_input_map(input_params)

    def _create_service_catalog_state_machine_input_map(
            self, portfolio, product):
        input_params = {}

        sc_portfolio = {}
        sc_portfolio.update({'PortfolioName': sanitize(portfolio.name, True)})
        sc_portfolio.update(
            {'PortfolioDescription': sanitize(portfolio.description, True)})
        sc_portfolio.update(
            {'PortfolioProvider': sanitize(portfolio.owner, True)})
        ssm_value = self.param_handler.update_params(
            transform_params({'principal_role': portfolio.principal_role}))
        sc_portfolio.update({'PrincipalArn': ssm_value.get('principal_role')})

        sc_product = {}
        sc_product.update({'ProductName': sanitize(product.name, True)})
        sc_product.update({'ProductDescription': product.description})
        sc_product.update({'ProductOwner': sanitize(portfolio.owner, True)})
        if product.hide_old_versions is True:
            sc_product.update({'HideOldVersions': 'Yes'})
        else:
            sc_product.update({'HideOldVersions': 'No'})
        ssm_value = self.param_handler.update_params(
            transform_params(
                {'launch_constraint_role': product.launch_constraint_role}))
        sc_product.update({'RoleArn': ssm_value.get('launch_constraint_role')})

        ec2 = EC2(self.logger, environ.get('AWS_REGION'))
        region_list = []
        for region in ec2.describe_regions():
            region_list.append(region.get('RegionName'))

        if os.path.isfile(
                os.path.join(self.manifest_folder, product.skeleton_file)):
            lambda_arn_param = get_env_var('lambda_arn_param_name')
            lambda_arn = self.ssm.get_parameter(lambda_arn_param)
            portfolio_index = self.manifest.portfolios.index(portfolio)
            product_index = self.manifest.portfolios[
                portfolio_index].products.index(product)
            product_name = self.manifest.portfolios[portfolio_index].products[
                product_index].name
            logger.info(
                "Generating the product template for {} from {}".format(
                    product_name,
                    os.path.join(self.manifest_folder, product.skeleton_file)))
            j2loader = jinja2.FileSystemLoader(self.manifest_folder)
            j2env = jinja2.Environment(loader=j2loader)
            j2template = j2env.get_template(product.skeleton_file)
            template_url = None
            if product.product_type.lower() == 'baseline':
                # j2result = j2template.render(manifest=self.manifest, portfolio_index=portfolio_index,
                #                              product_index=product_index, lambda_arn=lambda_arn, uuid=uuid.uuid4(),
                #                              regions=region_list)
                template_url = self._stage_template(product.skeleton_file +
                                                    ".template")
            elif product.product_type.lower() == 'optional':
                if len(product.template_file) > 0:
                    template_url = self._stage_template(product.template_file)
                    j2result = j2template.render(
                        manifest=self.manifest,
                        portfolio_index=portfolio_index,
                        product_index=product_index,
                        lambda_arn=lambda_arn,
                        uuid=uuid.uuid4(),
                        template_url=template_url)
                    generated_avm_template = os.path.join(
                        self.manifest_folder,
                        product.skeleton_file + ".generated.template")
                    logger.info(
                        "Writing the generated product template to {}".format(
                            generated_avm_template))
                    with open(generated_avm_template, "w") as fh:
                        fh.write(j2result)
                    template_url = self._stage_template(generated_avm_template)
                else:
                    raise Exception(
                        "Missing template_file location for portfolio:{} and product:{} in Manifest file"
                        .format(portfolio.name, product.name))

        else:
            raise Exception(
                "Missing skeleton_file for portfolio:{} and product:{} in Manifest file"
                .format(portfolio.name, product.name))

        artifact_params = {}
        artifact_params.update({'Info': {'LoadTemplateFromURL': template_url}})
        artifact_params.update({'Type': 'CLOUD_FORMATION_TEMPLATE'})
        artifact_params.update({'Description': product.description})
        sc_product.update({'ProvisioningArtifactParameters': artifact_params})

        try:
            if product.rules_file:
                rules = self._load_template_rules(product.rules_file)
                sc_product.update({'Rules': rules})
        except Exception as e:
            logger.error(e)

        input_params.update({'SCPortfolio': sc_portfolio})
        input_params.update({'SCProduct': sc_product})

        return self._create_state_machine_input_map(input_params)

    def _create_launch_avm_state_machine_input_map(self, portfolio, product,
                                                   accounts):
        input_params = {}
        input_params.update({'PortfolioName': sanitize(portfolio, True)})
        input_params.update({'ProductName': sanitize(product, True)})
        input_params.update({'ProvisioningParametersList': accounts})
        return self._create_state_machine_input_map(input_params)

    def _run_or_queue_state_machine(self, sm_input, sm_arn, list_sm_exec_arns,
                                    sm_name):
        logger.info("State machine Input: {}".format(sm_input))
        exec_name = "%s-%s-%s" % (sm_input.get('RequestType'),
                                  sm_name.replace(" ", ""),
                                  time.strftime("%Y-%m-%dT%H-%M-%S"))
        # If Sequential, kick off the first SM, and save the state machine input JSON
        # for the rest in SSM parameter store under /job_id/0 tree
        if self.isSequential:
            if self.index == 100:
                sm_input = self._populate_ssm_params(sm_input)
                sm_exec_arn = self.state_machine.trigger_state_machine(
                    sm_arn, sm_input, exec_name)
                list_sm_exec_arns.append(sm_exec_arn)
            else:
                param_name = "/%s/%s" % (self.token, self.index)
                self.ssm.put_parameter(param_name, json.dumps(sm_input))
        # Else if Parallel, execute all SM at regular interval of wait_time
        else:
            sm_input = self._populate_ssm_params(sm_input)
            sm_exec_arn = self.state_machine.trigger_state_machine(
                sm_arn, sm_input, exec_name)
            time.sleep(int(wait_time))  # Sleeping for sometime
            list_sm_exec_arns.append(sm_exec_arn)
        self.index = self.index + 1

    def _deploy_resource(self,
                         resource,
                         sm_arn,
                         list_sm_exec_arns,
                         account_id=None):
        template_full_path = self._stage_template(resource.template_file)
        params = {}
        deploy_resource_flag = True
        if resource.parameter_file:
            if len(resource.regions) > 0:
                params = self._load_params(resource.parameter_file, account_id,
                                           resource.regions[0])
            else:
                params = self._load_params(resource.parameter_file, account_id,
                                           self.manifest.region)

        ssm_map = self._create_ssm_input_map(resource.ssm_parameters)

        if account_id is not None:
            #Deploying Core resource Stack Set
            stack_name = "AWS-Landing-Zone-{}".format(resource.name)
            sm_input = self._create_stack_set_state_machine_input_map(
                stack_name, template_full_path, params, [str(account_id)],
                resource.regions, ssm_map)
        else:
            #Deploying Baseline resource Stack Set
            stack_name = "AWS-Landing-Zone-Baseline-{}".format(resource.name)
            sm_input = self._create_stack_set_state_machine_input_map(
                stack_name, template_full_path, params, [], [], ssm_map)

            stack_set = StackSet(self.logger)
            response = stack_set.describe_stack_set(stack_name)
            if response is not None:
                self.logger.info("Found existing stack set.")
                self.logger.info(
                    "Comparing the template of the StackSet: {} with local copy of template"
                    .format(stack_name))
                relative_template_path = resource.template_file
                if relative_template_path.lower().startswith('s3'):
                    local_template_file = self._download_remote_file(
                        relative_template_path)
                else:
                    local_template_file = os.path.join(self.manifest_folder,
                                                       relative_template_path)

                cfn_template_file = tempfile.mkstemp()[1]
                with open(cfn_template_file, "w") as f:
                    f.write(response.get('StackSet').get('TemplateBody'))

                template_compare = filecmp.cmp(local_template_file,
                                               cfn_template_file)
                self.logger.info(
                    "Comparing the parameters of the StackSet: {} with local copy of JSON parameters file"
                    .format(stack_name))
                params_compare = True
                if template_compare:
                    cfn_params = reverse_transform_params(
                        response.get('StackSet').get('Parameters'))
                    for key, value in params.items():
                        if cfn_params.get(key, '') == value:
                            pass
                        else:
                            params_compare = False
                            break

                self.logger.info(
                    "template_compare={}".format(template_compare))
                self.logger.info("params_compare={}".format(params_compare))
                if template_compare and params_compare:
                    deploy_resource_flag = False
                    self.logger.info(
                        "Found no changes in template & parameters, so skipping Update StackSet for {}"
                        .format(stack_name))

        if deploy_resource_flag:
            self._run_or_queue_state_machine(sm_input, sm_arn,
                                             list_sm_exec_arns, stack_name)

    def start_core_account_sm(self, sm_arn_account):
        try:
            logger.info("Setting the lock_down_stack_sets_role={}".format(
                self.manifest.lock_down_stack_sets_role))

            if self.manifest.lock_down_stack_sets_role is True:
                self.ssm.put_parameter('lock_down_stack_sets_role_flag', 'yes')
            else:
                self.ssm.put_parameter('lock_down_stack_sets_role_flag', 'no')

            # Send metric - pipeline run count
            data = {"PipelineRunCount": "1"}
            self.send.metrics(data)

            logger.info("Processing Core Accounts from {} file".format(
                self.manifest_file_path))
            list_sm_exec_arns = []
            for ou in self.manifest.organizational_units:
                ou_name = ou.name
                logger.info(
                    "Generating the state machine input json for OU: {}".
                    format(ou_name))

                if len(ou.core_accounts) == 0:
                    # Empty OU with no Accounts
                    sm_input = self._create_account_state_machine_input_map(
                        ou_name)
                    self._run_or_queue_state_machine(sm_input, sm_arn_account,
                                                     list_sm_exec_arns,
                                                     ou_name)

                for account in ou.core_accounts:
                    account_name = account.name

                    if account_name.lower() == 'primary':
                        account_email = ''
                    else:
                        account_email = account.email
                        if not account_email:
                            raise Exception(
                                "Failed to retrieve the email address for the Account: {}"
                                .format(account_name))

                    ssm_map = self._create_ssm_input_map(
                        account.ssm_parameters)

                    sm_input = self._create_account_state_machine_input_map(
                        ou_name, account_name, account_email, ssm_map)
                    self._run_or_queue_state_machine(sm_input, sm_arn_account,
                                                     list_sm_exec_arns,
                                                     account_name)
            self._save_sm_exec_arn(list_sm_exec_arns)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_core_resource_sm(self, sm_arn_stack_set):
        try:
            logger.info("Parsing Core Resources from {} file".format(
                self.manifest_file_path))
            list_sm_exec_arns = []
            count = 0
            for ou in self.manifest.organizational_units:
                for account in ou.core_accounts:
                    account_name = account.name
                    account_id = ''
                    for ssm_parameter in account.ssm_parameters:
                        if ssm_parameter.value == '$[AccountId]':
                            account_id = self.ssm.get_parameter(
                                ssm_parameter.name)

                    if account_id == '':
                        raise Exception(
                            "Missing required SSM parameter: {} to retrive the account Id of Account: {} defined in Manifest"
                            .format(ssm_parameter.name, account_name))

                    for resource in account.core_resources:
                        # Count number of stacksets
                        count += 1
                        if resource.deploy_method.lower() == 'stack_set':
                            self._deploy_resource(resource, sm_arn_stack_set,
                                                  list_sm_exec_arns,
                                                  account_id)
                        else:
                            raise Exception(
                                "Unsupported deploy_method: {} found for resource {} and Account: {} in Manifest"
                                .format(resource.deploy_method, resource.name,
                                        account_name))
            data = {"CoreAccountStackSetCount": str(count)}
            self.send.metrics(data)
            self._save_sm_exec_arn(list_sm_exec_arns)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_service_control_policy_sm(self, sm_arn_scp):
        try:
            logger.info("Processing SCPs from {} file".format(
                self.manifest_file_path))
            list_sm_exec_arns = []
            count = 0
            for policy in self.manifest.organization_policies:
                policy_content = self._load_policy(policy.policy_file)
                sm_input = self._create_service_control_policy_state_machine_input_map(
                    policy.name, policy_content, policy.description)
                self._run_or_queue_state_machine(sm_input, sm_arn_scp,
                                                 list_sm_exec_arns,
                                                 policy.name)
                # Count number of stacksets
                count += 1
            self._save_sm_exec_arn(list_sm_exec_arns)
            data = {"SCPPolicyCount": str(count)}
            self.send.metrics(data)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_service_catalog_sm(self, sm_arn_sc):
        try:
            logger.info(
                "Processing Service catalogs section from {} file".format(
                    self.manifest_file_path))
            list_sm_exec_arns = []
            for portfolio in self.manifest.portfolios:
                for product in portfolio.products:
                    sm_input = self._create_service_catalog_state_machine_input_map(
                        portfolio, product)
                    self._run_or_queue_state_machine(sm_input, sm_arn_sc,
                                                     list_sm_exec_arns,
                                                     product.name)
            self._save_sm_exec_arn(list_sm_exec_arns)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_baseline_resources_sm(self, sm_arn_stack_set):
        try:
            logger.info("Parsing Basline Resources from {} file".format(
                self.manifest_file_path))
            list_sm_exec_arns = []
            count = 0
            for resource in self.manifest.baseline_resources:
                if resource.deploy_method.lower() == 'stack_set':
                    self._deploy_resource(resource, sm_arn_stack_set,
                                          list_sm_exec_arns)
                    # Count number of stacksets
                    count += 1
                else:
                    raise Exception(
                        "Unsupported deploy_method: {} found for resource {} in Manifest"
                        .format(resource.deploy_method, resource.name))
            data = {"BaselineStackSetCount": str(count)}
            self.send.metrics(data)
            self._save_sm_exec_arn(list_sm_exec_arns)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_launch_avm(self, sm_arn_launch_avm):
        try:
            logger.info("Starting the launch AVM trigger")
            list_sm_exec_arns = []
            ou_id_map = {}

            org = Organizations(self.logger)
            response = org.list_roots()
            self.logger.info("List roots Response")
            self.logger.info(response)
            root_id = response['Roots'][0].get('Id')

            response = org.list_organizational_units_for_parent(
                ParentId=root_id)
            next_token = response.get('NextToken', None)

            for ou in response['OrganizationalUnits']:
                ou_id_map.update({ou.get('Name'): ou.get('Id')})

            while next_token is not None:
                response = org.list_organizational_units_for_parent(
                    ParentId=root_id, NextToken=next_token)
                next_token = response.get('NextToken', None)
                for ou in response['OrganizationalUnits']:
                    ou_id_map.update({ou.get('Name'): ou.get('Id')})

            self.logger.info("ou_id_map={}".format(ou_id_map))

            for portfolio in self.manifest.portfolios:
                for product in portfolio.products:
                    if product.product_type.lower() == 'baseline':
                        _params = self._load_params(product.parameter_file)
                        logger.info(
                            "Input parameters format for AVM: {}".format(
                                _params))
                        list_of_accounts = []
                        for ou in product.apply_baseline_to_accounts_in_ou:
                            self.logger.debug(
                                "Looking up ou={} in ou_id_map".format(ou))
                            ou_id = ou_id_map.get(ou)
                            self.logger.debug(
                                "ou_id={} for ou={} in ou_id_map".format(
                                    ou_id, ou))

                            response = org.list_accounts_for_parent(ou_id)
                            self.logger.debug(
                                "List Accounts for Parent Response")
                            self.logger.debug(response)
                            for account in response.get('Accounts'):
                                params = _params.copy()
                                for key, value in params.items():
                                    if value.lower() == 'accountemail':
                                        params.update(
                                            {key: account.get('Email')})
                                    elif value.lower() == 'accountname':
                                        params.update(
                                            {key: account.get('Name')})
                                    elif value.lower() == 'orgunitname':
                                        params.update({key: ou})

                                logger.info(
                                    "Input parameters format for Account: {} are {}"
                                    .format(account.get('Name'), params))

                                list_of_accounts.append(params)

                        if len(list_of_accounts) > 0:
                            sm_input = self._create_launch_avm_state_machine_input_map(
                                portfolio.name, product.name, list_of_accounts)
                            logger.info(
                                "Launch AVM state machine Input: {}".format(
                                    sm_input))
                            exec_name = "%s-%s-%s" % (
                                sm_input.get('RequestType'), "Launch-AVM",
                                time.strftime("%Y-%m-%dT%H-%M-%S"))
                            sm_exec_arn = self.state_machine.trigger_state_machine(
                                sm_arn_launch_avm, sm_input, exec_name)
                            list_sm_exec_arns.append(sm_exec_arn)

                    time.sleep(int(wait_time))  # Sleeping for sometime
            self._save_sm_exec_arn(list_sm_exec_arns)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def trigger_state_machines(self):
        try:
            self.manifest = Manifest(self.manifest_file_path)

            if self.pipeline_stage == 'core_accounts':
                self.start_core_account_sm(self.sm_arns_map.get('account'))
            elif self.pipeline_stage == 'core_resources':
                self.start_core_resource_sm(self.sm_arns_map.get('stack_set'))
            elif self.pipeline_stage == 'service_control_policy':
                self.start_service_control_policy_sm(
                    self.sm_arns_map.get('service_control_policy'))
            elif self.pipeline_stage == 'service_catalog':
                self.start_service_catalog_sm(
                    self.sm_arns_map.get('service_catalog'))
            elif self.pipeline_stage == 'baseline_resources':
                self.start_baseline_resources_sm(
                    self.sm_arns_map.get('stack_set'))
            elif self.pipeline_stage == 'launch_avm':
                self.start_launch_avm(self.sm_arns_map.get('launch_avm'))

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def get_state_machines_execution_status(self):
        try:
            sm_exec_arns = self.ssm.get_parameter(self.token)

            if sm_exec_arns == 'PASS':
                self.ssm.delete_parameter(self.token)
                return 'SUCCEEDED', ''
            else:
                list_sm_exec_arns = sm_exec_arns.split(
                    ","
                )  # Create a list from comma seperated string e.g. ['a','b','c']

                for sm_exec_arn in list_sm_exec_arns:
                    status = self.state_machine.check_state_machine_status(
                        sm_exec_arn)
                    if status == 'RUNNING':
                        return 'RUNNING', ''
                    elif status == 'SUCCEEDED':
                        continue
                    else:
                        self.ssm.delete_parameter(self.token)
                        self.ssm.delete_parameters_by_path(self.token)
                        err_msg = "State Machine Execution Failed, please check the Step function console for State Machine Execution ARN: {}".format(
                            sm_exec_arn)
                        return 'FAILED', err_msg

                if self.isSequential:
                    _params_list = self.ssm.get_parameters_by_path(self.token)
                    if _params_list:
                        params_list = sorted(_params_list,
                                             key=lambda i: i['Name'])
                        sm_input = json.loads(params_list[0].get('Value'))
                        if self.pipeline_stage == 'core_accounts':
                            sm_arn = self.sm_arns_map.get('account')
                            sm_name = sm_input.get('ResourceProperties').get(
                                'OUName') + "-" + sm_input.get(
                                    'ResourceProperties').get('AccountName')

                            account_name = sm_input.get(
                                'ResourceProperties').get('AccountName')
                            if account_name.lower() == 'primary':
                                org = Organizations(self.logger)
                                response = org.describe_account(
                                    self.primary_account_id)
                                account_email = response.get('Account').get(
                                    'Email', '')
                                sm_input.get('ResourceProperties').update(
                                    {'AccountEmail': account_email})

                        elif self.pipeline_stage == 'core_resources':
                            sm_arn = self.sm_arns_map.get('stack_set')
                            sm_name = sm_input.get('ResourceProperties').get(
                                'StackSetName')
                            sm_input = self._populate_ssm_params(sm_input)
                        elif self.pipeline_stage == 'service_control_policy':
                            sm_arn = self.sm_arns_map.get(
                                'service_control_policy')
                            sm_name = sm_input.get('ResourceProperties').get(
                                'PolicyDocument').get('Name')
                        elif self.pipeline_stage == 'service_catalog':
                            sm_arn = self.sm_arns_map.get('service_catalog')
                            sm_name = sm_input.get('ResourceProperties').get(
                                'SCProduct').get('ProductName')
                        elif self.pipeline_stage == 'baseline_resources':
                            sm_arn = self.sm_arns_map.get('stack_set')
                            sm_name = sm_input.get('ResourceProperties').get(
                                'StackSetName')
                            sm_input = self._populate_ssm_params(sm_input)

                        exec_name = "%s-%s-%s" % (sm_input.get(
                            'RequestType'), sm_name.replace(
                                " ", ""), time.strftime("%Y-%m-%dT%H-%M-%S"))
                        sm_exec_arn = self.state_machine.trigger_state_machine(
                            sm_arn, sm_input, exec_name)
                        self._save_sm_exec_arn([sm_exec_arn])
                        self.ssm.delete_parameter(params_list[0].get('Name'))
                        return 'RUNNING', ''

                self.ssm.delete_parameter(self.token)
                return 'SUCCEEDED', ''

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise
class LaunchSCP(object):
    def __init__(self, logger, wait_time, manifest_file_path, sm_arn_scp,
                 staging_bucket):
        self.state_machine = StateMachine(logger)
        self.s3 = S3(logger)
        self.send = Metrics(logger)
        self.param_handler = ParamsHandler(logger)
        self.logger = logger
        self.manifest_file_path = manifest_file_path
        self.manifest_folder = manifest_file_path[:-len(MANIFEST_FILE_NAME)]
        self.wait_time = wait_time
        self.sm_arn_scp = sm_arn_scp
        self.manifest = None
        self.list_sm_exec_arns = []
        self.nested_ou_delimiter = ""
        self.staging_bucket = staging_bucket
        self.root_id = None

    def _create_service_control_policy_state_machine_input_map(
            self, policy_name, policy_full_path, policy_desc='', ou_list=[]):
        input_params = {}
        policy_doc = {}
        policy_doc.update({'Name': sanitize(policy_name)})
        policy_doc.update({'Description': policy_desc})
        policy_doc.update({'PolicyURL': policy_full_path})
        input_params.update({'PolicyDocument': policy_doc})
        input_params.update({'AccountId': ''})
        input_params.update({'PolicyList': []})
        input_params.update({'Operation': ''})
        input_params.update({'OUList': ou_list})
        input_params.update({'OUNameDelimiter': self.nested_ou_delimiter})
        return self._create_state_machine_input_map(input_params)

    def _create_state_machine_input_map(self,
                                        input_params,
                                        request_type='Create'):
        request = {}
        request.update({'RequestType': request_type})
        request.update({'ResourceProperties': input_params})

        return request

    def _stage_template(self, relative_template_path):
        if relative_template_path.lower().startswith('s3'):
            # Convert the S3 URL s3://bucket-name/object
            # to HTTP URL https://s3.amazonaws.com/bucket-name/object
            s3_url = convert_s3_url_to_http_url(relative_template_path)
        else:
            local_file = os.path.join(self.manifest_folder,
                                      relative_template_path)
            # remote_file = "{}/{}_{}".format(TEMPLATE_KEY_PREFIX, self.token, relative_template_path[relative_template_path.rfind('/')+1:])
            remote_file = "{}/{}".format(TEMPLATE_KEY_PREFIX,
                                         relative_template_path)
            logger.info(
                "Uploading the template file: {} to S3 bucket: {} and key: {}".
                format(local_file, self.staging_bucket, remote_file))
            self.s3.upload_file(self.staging_bucket, local_file, remote_file)
            s3_url = "{}{}{}{}".format('https://s3.amazonaws.com/',
                                       self.staging_bucket, '/', remote_file)
        return s3_url

    def _run_or_queue_state_machine(self, sm_input, sm_arn, sm_name):
        logger.info("State machine Input: {}".format(sm_input))
        exec_name = "%s-%s-%s" % (sm_input.get('RequestType'),
                                  trim_length(sm_name.replace(" ", ""), 50),
                                  time.strftime("%Y-%m-%dT%H-%M-%S"))

        # execute all SM at regular interval of wait_time
        sm_exec_arn = self.state_machine.trigger_state_machine(
            sm_arn, sm_input, exec_name)
        time.sleep(int(wait_time))  # Sleeping for sometime
        self.list_sm_exec_arns.append(sm_exec_arn)

    def trigger_service_control_policy_state_machine(self):
        try:
            self.manifest = Manifest(self.manifest_file_path)
            self.start_service_control_policy_sm()
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def monitor_state_machines_execution_status(self):
        try:
            if self.list_sm_exec_arns:
                final_status = 'RUNNING'

                while final_status == 'RUNNING':
                    for sm_exec_arn in self.list_sm_exec_arns:
                        status = self.state_machine.check_state_machine_status(
                            sm_exec_arn)
                        if status == 'RUNNING':
                            final_status = 'RUNNING'
                            time.sleep(int(wait_time))
                            break
                        else:
                            final_status = 'COMPLETED'

                err_flag = False
                failed_sm_execution_list = []
                for sm_exec_arn in self.list_sm_exec_arns:
                    status = self.state_machine.check_state_machine_status(
                        sm_exec_arn)
                    if status == 'SUCCEEDED':
                        continue
                    else:
                        failed_sm_execution_list.append(sm_exec_arn)
                        err_flag = True
                        continue

                if err_flag:
                    return 'FAILED', failed_sm_execution_list
                else:
                    return 'SUCCEEDED', ''
            else:
                self.logger.info(
                    "SM Execution List {} is empty, nothing to monitor.".
                    format(self.list_sm_exec_arns))
                return None, []
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_service_control_policy_sm(self):
        try:
            logger.info("Processing SCPs from {} file".format(
                self.manifest_file_path))
            count = 0

            for policy in self.manifest.organization_policies:
                # Generate the list of OUs to attach this SCP to
                ou_list = []
                attach_ou_list = set(policy.apply_to_accounts_in_ou)

                for ou in attach_ou_list:
                    ou_list.append((ou, 'Attach'))

                policy_full_path = self._stage_template(policy.policy_file)
                sm_input = self._create_service_control_policy_state_machine_input_map(
                    policy.name, policy_full_path, policy.description, ou_list)
                self._run_or_queue_state_machine(sm_input, sm_arn_scp,
                                                 policy.name)

                # Count number of SCPs
                count += 1

            data = {"SCPPolicyCount": str(count)}
            self.send.metrics(data)

            # Exit where there are no organization policies
            if count == 0:
                logger.info("No organization policies are found.")
                sys.exit(0)
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise
Exemplo n.º 3
0
class LaunchAVM(object):
    def __init__(self, logger, wait_time, manifest_file_path, sm_arn_launch_avm, batch_size):
        self.state_machine = StateMachine(logger)
        self.ssm = SSM(logger)
        self.param_handler = ParamsHandler(logger)
        self.logger = logger
        self.manifest_file_path = manifest_file_path
        self.manifest_folder = manifest_file_path[:-len(MANIFEST_FILE_NAME)]
        self.wait_time = wait_time
        self.sm_arn_launch_avm = sm_arn_launch_avm
        self.manifest = None
        self.list_sm_exec_arns = []
        self.batch_size = batch_size
        self.avm_product_name = None
        self.avm_portfolio_name = None
        self.avm_params = None
        self.root_id = None

    def _load_params(self, relative_parameter_path, account = None, region = None):
        parameter_file = os.path.join(self.manifest_folder, relative_parameter_path)

        self.logger.info("Parsing the parameter file: {}".format(parameter_file))

        with open(parameter_file, 'r') as content_file:
            parameter_file_content = content_file.read()

        params = json.loads(parameter_file_content)
        # The last parameter is set to False, because we do not want to replace the SSM parameter values yet.
        sm_params = self.param_handler.update_params(params, account, region, False)

        self.logger.info("Input Parameters for State Machine: {}".format(sm_params))
        return sm_params

    def _create_state_machine_input_map(self, input_params, request_type='Create'):
        request = {}
        request.update({'RequestType':request_type})
        request.update({'ResourceProperties':input_params})
        return request

    def _create_launch_avm_state_machine_input_map(self, portfolio, product, accounts):
        input_params = {}
        input_params.update({'PortfolioName': sanitize(portfolio, True)})
        input_params.update({'ProductName': sanitize(product, True)})
        input_params.update({'ProvisioningParametersList': accounts})
        return self._create_state_machine_input_map(input_params)

    def _process_accounts_in_batches(self, accounts, organizations, ou_id, ou_name):
        try:
            list_of_accounts = []
            for account in accounts:
                if account.get('Status').upper() == 'SUSPENDED':
                    organizations.move_account(account.get('Id'), ou_id, self.root_id)
                    continue
                else:
                    params = self.avm_params.copy()
                    for key, value in params.items():
                        if value.lower() == 'accountemail':
                            params.update({key: account.get('Email')})
                        elif value.lower() == 'accountname':
                            params.update({key: account.get('Name')})
                        elif value.lower() == 'orgunitname':
                            params.update({key: ou_name})

                    self.logger.info(
                        "Input parameters format for Account: {} are {}".format(account.get('Name'), params))

                    list_of_accounts.append(params)

            if len(list_of_accounts) > 0:
                sm_input = self._create_launch_avm_state_machine_input_map(self.avm_portfolio_name,
                                                                           self.avm_product_name.strip(),
                                                                           list_of_accounts)
                self.logger.info("Launch AVM state machine Input: {}".format(sm_input))
                exec_name = "%s-%s-%s-%s" % (sm_input.get('RequestType'), sanitize(ou_name), "Launch-AVM",
                                             time.strftime("%Y-%m-%dT%H-%M-%S"))
                sm_exec_arn = self.state_machine.trigger_state_machine(self.sm_arn_launch_avm, sm_input, exec_name)
                self.list_sm_exec_arns.append(sm_exec_arn)

                time.sleep(int(wait_time))  # Sleeping for sometime
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def start_launch_avm(self):
        try:
            self.logger.info("Starting the launch AVM trigger")

            org = Org({}, self.logger)
            organizations = Organizations(self.logger)
            delimiter = self.manifest.nested_ou_delimiter

            response = organizations.list_roots()
            self.logger.info("List roots Response")
            self.logger.info(response)
            self.root_id = response['Roots'][0].get('Id')

            for ou in self.manifest.organizational_units:
                self.avm_product_name = ou.include_in_baseline_products[0]

                # Find the AVM for this OU and get the AVM parameters
                for portfolio in self.manifest.portfolios:
                    for product in portfolio.products:
                        if product.name.strip() == self.avm_product_name.strip():
                            self.avm_params = self._load_params(product.parameter_file)
                            self.avm_portfolio_name = portfolio.name.strip()

                if len(self.avm_params) == 0:
                    raise Exception("Baseline product: {} for OU: {} is not found in the" \
                      " portfolios section of Manifest".format(self.avm_product, ou.name))

                ou_id = org._get_ou_id(organizations, self.root_id, ou.name, delimiter)

                self.logger.info("Processing Accounts under: {} in batches of size: {}".format(ou_id, self.batch_size))
                response = organizations.list_accounts_for_parent(ou_id, self.batch_size)
                self.logger.info("List Accounts for Parent OU {} Response".format(ou_id))
                self.logger.info(response)
                self._process_accounts_in_batches(response.get('Accounts'), organizations, ou_id, ou.name)
                next_token = response.get('NextToken', None)

                while next_token is not None:
                    self.logger.info("Next Token Returned: {}".format(next_token))
                    response = organizations.list_accounts_for_parent(ou_id, self.batch_size, next_token)
                    self.logger.info("List Accounts for Parent OU {} Response".format(ou_id))
                    self.logger.info(response)
                    self._process_accounts_in_batches(response.get('Accounts'), organizations, ou_id, ou.name)
                    next_token = response.get('NextToken', None)

            return
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def trigger_launch_avm_state_machine(self):
        try:
            self.manifest = Manifest(self.manifest_file_path)
            self.start_launch_avm()
            return
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def monitor_state_machines_execution_status(self):
        try:
            final_status = 'RUNNING'

            while final_status == 'RUNNING':
                for sm_exec_arn in self.list_sm_exec_arns:
                    status = self.state_machine.check_state_machine_status(sm_exec_arn)
                    if status == 'RUNNING':
                        final_status = 'RUNNING'
                        time.sleep(int(wait_time))
                        break
                    else:
                        final_status = 'COMPLETED'

            err_flag = False
            failed_sm_execution_list = []
            for sm_exec_arn in self.list_sm_exec_arns:
                status = self.state_machine.check_state_machine_status(sm_exec_arn)
                if status == 'SUCCEEDED':
                    continue
                else:
                    failed_sm_execution_list.append(sm_exec_arn)
                    err_flag = True
                    continue

            if err_flag:
                return 'FAILED', failed_sm_execution_list
            else:
                return 'SUCCEEDED', ''

        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise
Exemplo n.º 4
0
class LaunchAVM(object):
    def __init__(self, logger, wait_time, manifest_file_path,
                 sm_arn_launch_avm, batch_size):
        self.state_machine = StateMachine(logger)
        self.ssm = SSM(logger)
        self.sc = SC(logger)
        self.param_handler = CFNParamsHandler(logger)
        self.logger = logger
        self.manifest_file_path = manifest_file_path
        self.manifest_folder = manifest_file_path[:-len(MANIFEST_FILE_NAME)]
        self.wait_time = wait_time
        self.sm_arn_launch_avm = sm_arn_launch_avm
        self.manifest = None
        self.list_sm_exec_arns = []
        self.batch_size = batch_size
        self.avm_product_name = None
        self.avm_product_id = None
        self.avm_artifact_id = None
        self.avm_params = None
        self.root_id = None
        self.sc_portfolios = {}
        self.sc_products = {}
        self.provisioned_products = {}  # [productid] = []
        self.provisioned_products_by_account = {
        }  # [account] = [] list of ppids

    def _load_params(self, relative_parameter_path, account=None, region=None):
        parameter_file = os.path.join(self.manifest_folder,
                                      relative_parameter_path)

        self.logger.info(
            "Parsing the parameter file: {}".format(parameter_file))

        with open(parameter_file, 'r') as content_file:
            parameter_file_content = content_file.read()

        params = json.loads(parameter_file_content)
        # The last parameter is set to False, because we do not want to replace the SSM parameter values yet.
        sm_params = self.param_handler.update_params(params, account, region,
                                                     False)

        self.logger.info(
            "Input Parameters for State Machine: {}".format(sm_params))
        return sm_params

    def _create_launch_avm_state_machine_input_map(self, accounts):
        """
        Create the input parameters for the state machine
        """
        portfolio = self.avm_portfolio_name
        product = self.avm_product_name.strip()

        request = {}
        request.update({'RequestType': 'Create'})
        request.update({'PortfolioId': self.sc_portfolios.get(portfolio)})

        portfolio_exist = False
        if any(self.sc_portfolios.get(portfolio)):
            portfolio_exist = True
        request.update({'PortfolioExist': portfolio_exist})

        request.update(
            {'ProductId': self.sc_products.get(portfolio).get(product)})
        request.update({
            'ProvisioningArtifactId':
            self._get_provisioning_artifact_id(request.get('ProductId'))
        })

        product_exist = False
        if any(self.sc_products.get(portfolio).get(product)):
            product_exist = True
        request.update({'ProductExist': product_exist})

        input_params = {}
        input_params.update({'PortfolioName': sanitize(portfolio, True)})
        input_params.update({'ProductName': sanitize(product, True)})
        input_params.update({'ProvisioningParametersList': accounts})

        request.update({'ResourceProperties': input_params})
        # Set up the iteration parameters for the state machine
        request.update({'Index': 0})
        request.update({'Step': 1})
        request.update(
            {'Count': len(input_params['ProvisioningParametersList'])})

        return request

    def _get_provisioning_artifact_id(self, product_id):
        self.logger.info("Listing the provisioning artifact")
        response = self.sc.list_provisioning_artifacts(product_id)
        self.logger.info("List Artifacts Response")
        self.logger.info(response)

        version_list = response.get('ProvisioningArtifactDetails')
        if version_list:
            return version_list[-1].get('Id')
        else:
            raise Exception("Unable to find provisioning artifact id.")

    def _portfolio_in_manifest(self, portname):
        """
        Scan the list of portfolios in the manifest looking for a match
        to portname
        """
        portname = portname.strip()
        self.logger.debug('Looking for portfolio {}'.format(portname))
        exists = False
        for port in self.manifest.portfolios:
            if portname == port.name.strip():
                exists = True
                break
        return exists

    def _product_in_manifest(self, portname, productname):
        """
        Scan the list of products in the portfolio in the manifest looking
        for a match to product name
        """
        portname = portname.strip()
        productname = productname.strip()
        self.logger.debug('Looking for product {} in portfolio {}'.format(
            productname, portname))
        exists = False
        for port in self.manifest.portfolios:
            if portname == port.name.strip():
                for product in port.products:
                    if productname == product.name.strip():
                        self.logger.debug('MATCH')
                        exists = True
                        break
                break
        return exists

    def sc_lookup(self):
        """
        Using data from input_params gather ServiceCatalog product info.
        The product data is used when creating the json data to hand off
        to LaunchAVM state machine
        """
        try:

            response = self.sc.list_portfolios()
            portfolio_list = response.get('PortfolioDetails')

            for portfolio in portfolio_list:
                portfolio_name = portfolio.get('DisplayName')

                # Is this portfolio in the manifest? If not skip it.
                if not self._portfolio_in_manifest(portfolio_name):
                    continue

                portfolio_id = portfolio.get('Id')
                self.sc_portfolios.update({portfolio_name: portfolio_id})

                # Initialize the portfolio in the products dictionary
                self.sc_products.update({portfolio_name: {}})

                # Get info for the products in this portfolio
                response = self.sc.search_products_as_admin(portfolio_id)

                product_list = response.get('ProductViewDetails')

                # find the product in the portfolio and add it to the dictionary
                for product in product_list:
                    portfolio_product_name = product.get(
                        'ProductViewSummary').get('Name')
                    if not self._product_in_manifest(portfolio_name,
                                                     portfolio_product_name):
                        continue

                    product_id = product['ProductViewSummary'].get('ProductId')

                    # add the product to the sc_products dictionary
                    self.sc_products[portfolio_name].update(
                        {portfolio_product_name: product_id})

            self.logger.debug('DUMP OF SC_PRODUCTS')
            self.logger.debug(json.dumps(self.sc_products, indent=2))

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'CLASS': self.__class__.__name__,
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def get_indexed_provisioned_products(self):
        """
        Get all provisioned products for a Service Catalog product Id.
        Create an index by provisioned_product_id.

        This data is the same for every account for the same product Id.

        Ref: state_machine_handler::search_provisioned_products@2031
        """
        try:
            # 1) Get a complete list of provisioned products for this product Id

            pprods = self.sc.search_provisioned_products(self.avm_product_id)
            token = 'init'

            while token:

                for provisioned_product in pprods.get('ProvisionedProducts'):
                    self.logger.info('PROCESSING ' +
                                     str(provisioned_product['Id']))
                    self.logger.debug(
                        "ProvisionedProduct:{}".format(provisioned_product))
                    provisioned_product_id = provisioned_product.get('Id')

                    # 2) Remove any with a status of ERROR or UNDER_CHANGE
                    # Ignore products that error out before and
                    # to avoid the case of looking up the same product ignore UNDER_CHANGE
                    if provisioned_product.get(
                            'Status') == 'ERROR' or provisioned_product.get(
                                'Status') == 'UNDER_CHANGE':
                        continue

                    # This provisioned product passes - add it to the dict
                    # We only reference AccountEmail and ExistingParameterKeys in StackInfo
                    self.provisioned_products[provisioned_product_id] = {}

                    # 3) Extract stack_name from stack_id (see state_machine_handler@2066)
                    stack_id = provisioned_product.get('PhysicalId')
                    self.logger.debug("stack_id={}".format(stack_id))

                    # Extract Stack Name from the Physical Id
                    # e.g. Stack Id: arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/SC-${AWS::AccountId}-pp-fb3xte4fc4jmk/5790fb30-547b-11e8-b302-50fae98974c5
                    # Stack name = SC-${AWS::AccountId}-pp-fb3xte4fc4jmk
                    stack_name = stack_id.split('/')[1]
                    self.logger.debug("stack_name={}".format(stack_name))

                    # 4) Query stack state and add AccountEmail, ExistingParameterKeys (see shm@2097)
                    self.provisioned_products[
                        provisioned_product_id] = get_stack_data(
                            stack_name, self.logger)

                    # Add the provisioned product Id to key/value keyed by account
                    # Note: by intentional limitation there is exactly one provisioned product
                    #   per Product Id in ALZ
                    account_email = self.provisioned_products[
                        provisioned_product_id].get('AccountEmail', None)
                    if account_email:
                        self.provisioned_products_by_account[
                            account_email] = provisioned_product_id

                token = pprods.get('NextPageToken', None)
                pprods = None  # Reset
                if token:
                    pprods = self.sc.search_provisioned_products(
                        self.avm_product_id, token)

            self.logger.debug('DUMP OF PROVISIONED PRODUCTS')
            self.logger.debug(
                json.dumps(self.provisioned_products,
                           indent=2,
                           default=date_handler))

            self.logger.debug('DUMP OF PROVISIONED PRODUCTS INDEX')
            self.logger.debug(
                json.dumps(self.provisioned_products_by_account,
                           indent=2,
                           default=date_handler))

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)

    def _process_accounts_in_batches(self, accounts, organizations, ou_id,
                                     ou_name):
        """
        Each account in an OU is processed into a batch of one or more accounts.
        This function processes one batch.

        For each account:
            get email, name, ou name
            ignore suspended accounts
            build state machine input
            instantiate state machine

        Note: sm_input must not exceed 32K max
        """
        try:
            list_of_accounts = []
            for account in accounts:
                # Process each account
                if account.get('Status').upper() == 'SUSPENDED':
                    # Account is suspended
                    organizations.move_account(account.get('Id'), ou_id,
                                               self.root_id)
                    continue
                else:
                    # Active account
                    params = self.avm_params.copy()
                    for key, value in params.items():
                        if value.lower() == 'accountemail':
                            params.update({key: account.get('Email')})
                        elif value.lower() == 'accountname':
                            params.update({key: account.get('Name')})
                        elif value.lower() == 'orgunitname':
                            params.update({key: ou_name})

                    # Retrieve the provisioned product id
                    ppid = self.provisioned_products_by_account.get(
                        account.get('Email'), None)
                    if ppid:
                        params.update({'ProvisionedProductId': ppid})
                        params.update({'ProvisionedProductExists': True})
                        params.update({
                            'ExistingParameterKeys':
                            self.provisioned_products.get(ppid).get(
                                'ExistingParameterKeys', [])
                        })
                    else:
                        params.update({'ProvisionedProductExists': False})

                    self.logger.info(
                        "Input parameters format for Account: {} are {}".
                        format(account.get('Name'), params))

                    list_of_accounts.append(params)

            if list_of_accounts:
                # list_of_accounts is passed directly through to the input json data
                # This data should be complete from start_launch_avm
                sm_input = self._create_launch_avm_state_machine_input_map(
                    # self.avm_portfolio_name,
                    # self.avm_product_name.strip(),
                    list_of_accounts)
                self.logger.info(
                    "Launch AVM state machine Input: {}".format(sm_input))
                exec_name = "%s-%s-%s-%s-%s" % (
                    "AVM",
                    sanitize(ou_name[:40]),
                    time.strftime("%Y-%m-%dT%H-%M-%S"),
                    str(time.time()).split('.')[1],  # append microsecond
                    str(uuid4()).split('-')[1])
                sm_exec_arn = self.state_machine.trigger_state_machine(
                    self.sm_arn_launch_avm, sm_input, exec_name)
                self.list_sm_exec_arns.append(sm_exec_arn)

                time.sleep(int(self.wait_time))  # Sleeping for sometime

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def start_launch_avm(self):
        """
        Get a list of accounts
        Find the portfolio id and product id for the AVM product
        Call _process_accounts_in_batches to build and submit input data for
        each batch to a state machine instance
        """
        try:
            self.logger.info("Starting the launch AVM trigger")

            org = Org({}, self.logger)
            organizations = Organizations(self.logger)
            delimiter = ':'
            if self.manifest.nested_ou_delimiter:
                delimiter = self.manifest.nested_ou_delimiter

            response = organizations.list_roots()
            self.logger.debug("List roots Response")
            self.logger.debug(response)
            self.root_id = response['Roots'][0].get('Id')

            for ou in self.manifest.organizational_units:
                self.avm_product_name = ou.include_in_baseline_products[0]

                # Find the AVM for this OU and get the AVM parameters
                for portfolio in self.manifest.portfolios:
                    for product in portfolio.products:
                        if product.name.strip() == self.avm_product_name.strip(
                        ):
                            self.avm_params = self._load_params(
                                product.parameter_file)
                            self.avm_portfolio_name = portfolio.name.strip()
                            self.avm_product_id = self.sc_products.get(
                                portfolio.name.strip()).get(
                                    product.name.strip())
                """
                Get provisioned product data for all accounts
                Note: this reduces the number of API calls, but produces a large
                in-memory dictionary. However, even at 1,000 accounts this should
                not be a concern
                Populates:
                self.provisioned_products = {}              # [productid] = []
                self.provisioned_products_by_account = {}   # [account] = [] list of ppids
                self.stacks = {}                            # [stackname] = stackinfo
                """
                self.get_indexed_provisioned_products()

                if not self.avm_params:
                    raise Exception("Baseline product: {} for OU: {} is not found in the" \
                      " portfolios section of Manifest".format(self.avm_product_name, ou.name))

                ou_id = org._get_ou_id(organizations, self.root_id, ou.name,
                                       delimiter)

                self.logger.info(
                    "Processing Accounts under: {} in batches of size: {}".
                    format(ou_id, self.batch_size))
                response = organizations.list_accounts_for_parent(
                    ou_id, self.batch_size)
                self.logger.info(
                    "List Accounts for Parent OU {} Response".format(ou_id))
                self.logger.info(response)
                self._process_accounts_in_batches(response.get('Accounts'),
                                                  organizations, ou_id,
                                                  ou.name)
                next_token = response.get('NextToken', None)

                while next_token is not None:
                    self.logger.info(
                        "Next Token Returned: {}".format(next_token))
                    response = organizations.list_accounts_for_parent(
                        ou_id, self.batch_size, next_token)
                    self.logger.info(
                        "List Accounts for Parent OU {} Response".format(
                            ou_id))
                    self.logger.info(response)
                    self._process_accounts_in_batches(response.get('Accounts'),
                                                      organizations, ou_id,
                                                      ou.name)
                    next_token = response.get('NextToken', None)

            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def trigger_launch_avm_state_machine(self):
        try:
            self.manifest = Manifest(self.manifest_file_path)
            self.sc_lookup()  # Get Service Catalog data
            self.start_launch_avm()
            return
        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise

    def monitor_state_machines_execution_status(self):
        try:
            final_status = 'RUNNING'

            while final_status == 'RUNNING':
                for sm_exec_arn in self.list_sm_exec_arns:
                    if self.state_machine.check_state_machine_status(
                            sm_exec_arn) == 'RUNNING':
                        final_status = 'RUNNING'
                        time.sleep(int(wait_time))
                        break
                    else:
                        final_status = 'COMPLETED'

            err_flag = False
            failed_sm_execution_list = []
            for sm_exec_arn in self.list_sm_exec_arns:
                if self.state_machine.check_state_machine_status(
                        sm_exec_arn) == 'SUCCEEDED':
                    continue
                else:
                    failed_sm_execution_list.append(sm_exec_arn)
                    err_flag = True
                    continue

            if err_flag:
                result = ['FAILED', failed_sm_execution_list]
            else:
                result = ['SUCCEEDED', '']

            return result

        except Exception as e:
            message = {
                'FILE': __file__.split('/')[-1],
                'METHOD': inspect.stack()[0][3],
                'EXCEPTION': str(e)
            }
            self.logger.exception(message)
            raise
class DeployStackSetStateMachine(object):
    def __init__(self, logger, wait_time, manifest_file_path, sm_arn_stackset, staging_bucket, execution_mode):
        self.state_machine = StateMachine(logger)
        self.ssm = SSM(logger)
        self.s3 = S3(logger)
        self.send = Metrics(logger)
        self.param_handler = ParamsHandler(logger)
        self.logger = logger
        self.manifest_file_path = manifest_file_path
        self.manifest_folder = manifest_file_path[:-len(MANIFEST_FILE_NAME)]
        self.wait_time = wait_time
        self.sm_arn_stackset = sm_arn_stackset
        self.manifest = None
        self.list_sm_exec_arns = []
        self.staging_bucket = staging_bucket
        self.root_id = None
        self.uuid = uuid4()
        self.state_machine_event = {}
        if execution_mode.lower() == 'sequential':
            self.logger.info("Running {} mode".format(execution_mode))
            self.sequential_flag = True
        else:
            self.logger.info("Running {} mode".format(execution_mode))
            self.sequential_flag = False

    def _stage_template(self, relative_template_path):
        try:
            if relative_template_path.lower().startswith('s3'):
                # Convert the S3 URL s3://bucket-name/object to HTTP URL https://s3.amazonaws.com/bucket-name/object
                s3_url = convert_s3_url_to_http_url(relative_template_path)
            else:
                local_file = os.path.join(self.manifest_folder, relative_template_path)
                remote_file = "{}/{}_{}".format(TEMPLATE_KEY_PREFIX, self.uuid, relative_template_path)
                logger.info("Uploading the template file: {} to S3 bucket: {} and key: {}".format(local_file,
                                                                                                  self.staging_bucket,
                                                                                                  remote_file))
                self.s3.upload_file(self.staging_bucket, local_file, remote_file)
                s3_url = "{}{}{}{}".format('https://s3.amazonaws.com/', self.staging_bucket, '/', remote_file)
            return s3_url
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _load_params(self, relative_parameter_path, account=None, region=None):
        try:
            if relative_parameter_path.lower().startswith('s3'):
                parameter_file = download_remote_file(self.logger, relative_parameter_path)
            else:
                parameter_file = os.path.join(self.manifest_folder, relative_parameter_path)

            logger.info("Parsing the parameter file: {}".format(parameter_file))

            with open(parameter_file, 'r') as content_file:
                parameter_file_content = content_file.read()

            params = json.loads(parameter_file_content)
            if account is not None:
                # Deploying Core resource Stack Set
                # The last parameter is set to False, because we do not want to replace the SSM parameter values yet.
                sm_params = self.param_handler.update_params(params, account, region, False)
            else:
                # Deploying Baseline resource Stack Set
                sm_params = self.param_handler.update_params(params)

            logger.info("Input Parameters for State Machine: {}".format(sm_params))
            return sm_params
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _create_ssm_input_map(self, ssm_parameters):
        try:
            ssm_input_map = {}

            for ssm_parameter in ssm_parameters:
                key = ssm_parameter.name
                value = ssm_parameter.value
                ssm_value = self.param_handler.update_params(transform_params({key: value}))
                ssm_input_map.update(ssm_value)

            return ssm_input_map

        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _create_state_machine_input_map(self, input_params, request_type='Create'):
        try:
            self.state_machine_event.update({'RequestType': request_type})
            self.state_machine_event.update({'ResourceProperties': input_params})

        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _create_stack_set_state_machine_input_map(self, stack_set_name, template_url, parameters,
                                                  account_list, regions_list, ssm_map):
        input_params = {}
        input_params.update({'StackSetName': sanitize(stack_set_name)})
        input_params.update({'TemplateURL': template_url})
        input_params.update({'Parameters': parameters})
        input_params.update({'Capabilities': CAPABILITIES})

        if len(account_list) > 0:
            input_params.update({'AccountList': account_list})
            if len(regions_list) > 0:
                input_params.update({'RegionList': regions_list})
            else:
                input_params.update({'RegionList': [self.manifest.region]})
        else:
            input_params.update({'AccountList': ''})
            input_params.update({'RegionList': ''})

        if ssm_map is not None:
            input_params.update({'SSMParameters': ssm_map})

        self._create_state_machine_input_map(input_params)

    def _populate_ssm_params(self):
        try:
            # The scenario is if you have one core resource that exports output from CFN stack to SSM parameter
            # and then the next core resource reads the SSM parameter as input,
            # then it has to wait for the first core resource to
            # finish; read the SSM parameters and use its value as input for second core resource's input for SM
            # Get the parameters for CFN template from self.state_machine_event
            logger.debug("Populating SSM parameter values for SM input: {}".format(self.state_machine_event))
            params = self.state_machine_event.get('ResourceProperties').get('Parameters', {})
            # First transform it from {name: value} to [{'ParameterKey': name}, {'ParameterValue': value}]
            # then replace the SSM parameter names with its values
            sm_params = self.param_handler.update_params(transform_params(params))
            # Put it back into the self.state_machine_event
            self.state_machine_event.get('ResourceProperties').update({'Parameters': sm_params})
            logger.debug("Done populating SSM parameter values for SM input: {}".format(self.state_machine_event))

        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _compare_template_and_params(self):
        try:
            stack_name = self.state_machine_event.get('ResourceProperties').get('StackSetName', '')
            flag = False
            if stack_name:
                stack_set = StackSet(self.logger)
                describe_response = stack_set.describe_stack_set(stack_name)
                if describe_response is not None:
                    self.logger.info("Found existing stack set.")

                    self.logger.info("Checking the status of last stack set operation on {}".format(stack_name))
                    response = stack_set.list_stack_set_operations(StackSetName=stack_name,
                                                                   MaxResults=1)

                    if response:
                        if response.get('Summaries'):
                            for instance in response.get('Summaries'):
                                self.logger.info("Status of last stack set operation : {}"
                                                 .format(instance.get('Status')))
                                if instance.get('Status') != 'SUCCEEDED':
                                    self.logger.info("The last stack operation did not succeed. "
                                                     "Triggering Update StackSet for {}".format(stack_name))
                                    return False

                    self.logger.info("Comparing the template of the StackSet: {} with local copy of template"
                                     .format(stack_name))

                    template_http_url = self.state_machine_event.get('ResourceProperties').get('TemplateURL', '')
                    if template_http_url:
                        template_s3_url = convert_http_url_to_s3_url(template_http_url)
                        local_template_file = download_remote_file(self.logger, template_s3_url)
                    else:
                        self.logger.error("TemplateURL in state machine input is empty. Check state_machine_event:{}"
                                          .format(self.state_machine_event))
                        return False

                    cfn_template_file = tempfile.mkstemp()[1]
                    with open(cfn_template_file, "w") as f:
                        f.write(describe_response.get('StackSet').get('TemplateBody'))

                    template_compare = filecmp.cmp(local_template_file, cfn_template_file, False)
                    self.logger.info("Comparing the parameters of the StackSet: {} "
                                     "with local copy of JSON parameters file".format(stack_name))

                    params_compare = True
                    params = self.state_machine_event.get('ResourceProperties').get('Parameters', {})
                    if template_compare:
                        cfn_params = reverse_transform_params(describe_response.get('StackSet').get('Parameters'))
                        for key, value in params.items():
                            if cfn_params.get(key, '') == value:
                                pass
                            else:
                                params_compare = False
                                break

                    self.logger.info("template_compare={}".format(template_compare))
                    self.logger.info("params_compare={}".format(params_compare))
                    if template_compare and params_compare:
                        account_list = self.state_machine_event.get('ResourceProperties').get("AccountList", [])
                        if account_list:
                            self.logger.info("Comparing the Stack Instances Account & Regions for StackSet: {}"
                                             .format(stack_name))
                            expected_region_list = set(self.state_machine_event.get('ResourceProperties').get("RegionList", []))

                            # iterator over accounts in event account list
                            for account in account_list:
                                actual_region_list = set()

                                self.logger.info("### Listing the Stack Instances for StackSet: {} and Account: {} ###"
                                                 .format(stack_name, account))
                                stack_instance_list = stack_set.list_stack_instances_per_account(stack_name, account)

                                self.logger.info(stack_instance_list)

                                if stack_instance_list:
                                    for instance in stack_instance_list:
                                        if instance.get('Status').upper() == 'CURRENT':
                                            actual_region_list.add(instance.get('Region'))
                                        else:
                                            self.logger.info("Found at least one of the Stack Instances in {} state."
                                                             " Triggering Update StackSet for {}"
                                                             .format(instance.get('Status'),
                                                                     stack_name))
                                            return False
                                else:
                                    self.logger.info("Found no stack instances in account: {}, "
                                                     "Updating StackSet: {}".format(account, stack_name))
                                    # # move the account id to index 0
                                    # newindex = 0
                                    # oldindex = self.state_machine_event.get('ResourceProperties').get("AccountList").index(account)
                                    # self.state_machine_event.get('ResourceProperties').get("AccountList").insert(newindex, self.state_machine_event.get('ResourceProperties').get("AccountList").pop(oldindex))
                                    return False

                                if expected_region_list.issubset(actual_region_list):
                                    self.logger.info("Found expected regions : {} in deployed stack instances : {},"
                                                     " so skipping Update StackSet for {}"
                                                     .format(expected_region_list,
                                                             actual_region_list,
                                                             stack_name))
                                    flag = True
                        else:
                            self.logger.info("Found no changes in template & parameters, "
                                             "so skipping Update StackSet for {}".format(stack_name))
                            flag = True
            return flag
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def state_machine_failed(self, status, failed_execution_list):
        error = " StackSet State Machine Execution(s) Failed. Navigate to the AWS Step Functions console and" \
                " review the following State Machine Executions. ARN List: {}".format(failed_execution_list)
        if status == 'FAILED':
            logger.error(100 * '*')
            logger.error(error)
            logger.error(100 * '*')
            sys.exit(1)

    def _run_or_queue_state_machine(self, stackset_name):
        try:
            logger.info("State machine Input: {}".format(self.state_machine_event))
            exec_name = "%s-%s-%s" % (self.state_machine_event.get('RequestType'), trim_length(stackset_name.replace(" ", ""), 50),
                                      time.strftime("%Y-%m-%dT%H-%M-%S"))
            # If Sequential, wait for the SM to be executed before kicking of the next one
            if self.sequential_flag:
                self.logger.info(" > > > > > >  Running Sequential Mode. > > > > > >")
                self._populate_ssm_params()
                if self._compare_template_and_params():
                    return
                else:
                    sm_exec_arn = self.state_machine.trigger_state_machine(self.sm_arn_stackset, self.state_machine_event, exec_name)
                    self.list_sm_exec_arns.append(sm_exec_arn)
                    status, failed_execution_list = self.monitor_state_machines_execution_status()
                    if status == 'FAILED':
                        self.state_machine_failed(status, failed_execution_list)
                    else:
                        self.logger.info("State Machine execution completed. Starting next execution...")
            # Else if Parallel, execute all SM at regular interval of wait_time
            else:
                self.logger.info(" | | | | | |  Running Parallel Mode. | | | | | |")
                # RUNS Parallel, execute all SM at regular interval of wait_time
                self._populate_ssm_params()
                # if the stackset comparision is matches - skip SM execution
                if self._compare_template_and_params():
                    return
                else: # if False execution SM
                    sm_exec_arn = self.state_machine.trigger_state_machine(self.sm_arn_stackset, self.state_machine_event, exec_name)
                time.sleep(int(wait_time))  # Sleeping for sometime
                self.list_sm_exec_arns.append(sm_exec_arn)
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _deploy_resource(self, resource, account_list):
        try:
            template_full_path = self._stage_template(resource.template_file)
            params = {}
            if resource.parameter_file:
                if len(resource.regions) > 0:
                    params = self._load_params(resource.parameter_file, account_list, resource.regions[0])
                else:
                    params = self._load_params(resource.parameter_file, account_list, self.manifest.region)

            ssm_map = self._create_ssm_input_map(resource.ssm_parameters)

            # Deploying Core resource Stack Set
            stack_name = "CustomControlTower-{}".format(resource.name)
            self._create_stack_set_state_machine_input_map(stack_name, template_full_path,
                                                                      params, account_list, resource.regions, ssm_map)


            self.logger.info(" >>> State Machine Input >>>")
            self.logger.info(self.state_machine_event)

            self._run_or_queue_state_machine(stack_name)
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def _get_root_id(self, org):
        response = org.list_roots()
        self.logger.info("Response: List Roots")
        self.logger.info(response)
        return response['Roots'][0].get('Id')

    def _list_ou_for_parent(self, org, parent_id):
        _ou_list = org.list_organizational_units_for_parent(parent_id)
        self.logger.info("Print Organizational Units List under {}".format(parent_id))
        self.logger.info(_ou_list)
        return _ou_list

    def _get_accounts_in_ou(self, org, ou_id_list):
        _accounts_in_ou = []
        accounts_in_all_ous = []
        ou_id_to_account_map = {}

        for _ou_id in ou_id_list:
            _account_list = org.list_accounts_for_parent(_ou_id)
            for _account in _account_list:
                # filter ACTIVE and CREATED accounts
                if _account.get('Status') == "ACTIVE":
                    # create a list of accounts in OU
                    accounts_in_all_ous.append(_account.get('Id'))
                    _accounts_in_ou.append(_account.get('Id'))

            # create a map of accounts for each ou
            self.logger.info("Creating Key:Value Mapping - OU ID: {} ; Account List: {}"
                             .format(_ou_id, _accounts_in_ou))
            ou_id_to_account_map.update({_ou_id: _accounts_in_ou})
            self.logger.info(ou_id_to_account_map)

            # reset list of accounts in the OU
            _accounts_in_ou = []

        self.logger.info("All accounts in OU List: {}".format(accounts_in_all_ous))
        self.logger.info("OU to Account ID mapping")
        self.logger.info(ou_id_to_account_map)
        return accounts_in_all_ous, ou_id_to_account_map

    def _get_ou_ids(self, org):
        # for each OU get list of account
        # get root id
        root_id = self._get_root_id(org)

        # get OUs under the Org root
        ou_list_at_root_level = self._list_ou_for_parent(org, root_id)

        ou_id_list = []
        _ou_name_to_id_map = {}
        _all_ou_ids = []

        for ou_at_root_level in ou_list_at_root_level:
            # build list of all the OU IDs under Org root
            _all_ou_ids.append(ou_at_root_level.get('Id'))
            # build a list of ou id
            _ou_name_to_id_map.update({ou_at_root_level.get('Name'): ou_at_root_level.get('Id')})

        self.logger.info("Print OU Name to OU ID Map")
        self.logger.info(_ou_name_to_id_map)

        # return:
        # 1. OU IDs of the OUs in the manifest
        # 2. Account IDs in OUs in the manifest
        # 3. Account IDs in all the OUs in the manifest
        return _all_ou_ids, _ou_name_to_id_map

    def get_account_for_name(self, org):
        # get all accounts in the organization
        account_list = org.get_accounts_in_org()
        #self.logger.info("Print Account List: {}".format(account_list))

        _name_to_account_map = {}
        for account in account_list:
            if account.get("Status") == "ACTIVE":
                _name_to_account_map.update({account.get("Name"): account.get("Id")})

        self.logger.info("Print Account Name > Account Mapping")
        self.logger.info(_name_to_account_map)

        return _name_to_account_map

    def get_organization_details(self):
        # > build dict
        # KEY: OU Name (in the manifest)
        # VALUE: OU ID (at root level)
        # > build list
        # all OU IDs under root
        org = Organizations(self.logger)
        all_ou_ids, ou_name_to_id_map = self._get_ou_ids(org)
        # > build list of all active accounts
        # use case: use to validate accounts in the manifest file.
        # > build dict
        # KEY: OU ID (for each OU at root level)
        # VALUE: get list of all active accounts
        # use case: map OU Name to account IDs
        accounts_in_all_ous, ou_id_to_account_map = self._get_accounts_in_ou(org, all_ou_ids)
        # build dict
        # KEY: email
        # VALUE: account id
        # use case: convert email in manifest to account ID for SM event
        name_to_account_map = self.get_account_for_name(org)
        return accounts_in_all_ous, ou_id_to_account_map, ou_name_to_id_map, name_to_account_map

    def start_stackset_sm(self):
        try:
            logger.info("Parsing Core Resources from {} file".format(self.manifest_file_path))
            count = 0

            accounts_in_all_ous, ou_id_to_account_map, ou_name_to_id_map, name_to_account_map = self.get_organization_details()

            for resource in self.manifest.cloudformation_resources:
                self.logger.info(">>>>>>>>> START : {} >>>>>>>>>".format(resource.name))
                # Handle scenario if 'deploy_to_ou' key does not exist in the resource
                try:
                    self.logger.info(resource.deploy_to_ou)
                except:
                    resource.deploy_to_ou = []

                # Handle scenario if 'deploy_to_account' key does not exist in the resource
                try:
                    self.logger.info(resource.deploy_to_account)
                except:
                    resource.deploy_to_account = []

                # find accounts for given ou name
                accounts_in_ou = []
                ou_ids_manifest = []

                # check if OU name list is empty
                if resource.deploy_to_ou:
                    # convert OU Name to OU IDs
                    for ou_name in resource.deploy_to_ou:
                        ou_id = [value for key, value in ou_name_to_id_map.items() if ou_name.lower() in key.lower()]
                        ou_ids_manifest.extend(ou_id)

                    # convert OU IDs to accounts
                    for ou_id, accounts in ou_id_to_account_map.items():
                        if ou_id in ou_ids_manifest:
                            accounts_in_ou.extend(accounts)

                    self.logger.info(">>> Accounts: {} in OUs: {}".format(accounts_in_ou, resource.deploy_to_ou))

                # convert account numbers to string type
                account_list = self._convert_list_values_to_string(resource.deploy_to_account)
                self.logger.info(">>>>>> ACCOUNT LIST")
                self.logger.info(account_list)

                # separate account id and emails
                name_list = []
                new_account_list = []
                self.logger.info(account_list)
                for item in account_list:
                    if item.isdigit() and len(item) == 12:  # if an actual account ID
                        new_account_list.append(item)
                        self.logger.info(new_account_list)
                    else:
                        name_list.append(item)
                        self.logger.info(name_list)

                # check if name list is empty
                if name_list:
                    # convert OU Name to OU IDs
                    for name in name_list:
                        name_account = [value for key, value in name_to_account_map.items() if
                                         name.lower() in key.lower()]
                        self.logger.info("%%%%%%% Name {} -  Account {}".format(name, name_account))
                        new_account_list.extend(name_account)

                # Remove account ids from the manifest that is not in the organization or not active
                sanitized_account_list = list(set(new_account_list).intersection(set(accounts_in_all_ous)))
                self.logger.info("Print Updated Manifest Account List")
                self.logger.info(sanitized_account_list)

                # merge account lists manifest account list and accounts under OUs in the manifest
                sanitized_account_list.extend(accounts_in_ou)
                sanitized_account_list = list(set(sanitized_account_list)) # remove duplicate accounts
                self.logger.info("Print merged account list - accounts in manifest + account under OU in manifest")
                self.logger.info(sanitized_account_list)

                if resource.deploy_method.lower() == 'stack_set':
                    self._deploy_resource(resource, sanitized_account_list)
                else:
                    raise Exception("Unsupported deploy_method: {} found for resource {} and Account: {} in Manifest"
                                    .format(resource.deploy_method, resource.name, sanitized_account_list))
                self.logger.info("<<<<<<<<< FINISH : {} <<<<<<<<<".format(resource.name))

                # Count number of stack sets deployed
                count += 1
            data = {"StackSetCount": str(count)}
            self.send.metrics(data)

            return
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    # return list of strings
    def _convert_list_values_to_string(self, _list):
        return list(map(str, _list))

    # monitor list of state machine executions
    def monitor_state_machines_execution_status(self):
        try:
            if self.list_sm_exec_arns:
                self.logger.info("Starting to monitor the SM Executions: {}".format(self.list_sm_exec_arns))
                final_status = 'RUNNING'

                while final_status == 'RUNNING':
                    for sm_exec_arn in self.list_sm_exec_arns:
                        status = self.state_machine.check_state_machine_status(sm_exec_arn)
                        if status == 'RUNNING':
                            final_status = 'RUNNING'
                            time.sleep(int(wait_time))
                            break
                        else:
                            final_status = 'COMPLETED'

                err_flag = False
                failed_sm_execution_list = []
                for sm_exec_arn in self.list_sm_exec_arns:
                    status = self.state_machine.check_state_machine_status(sm_exec_arn)
                    if status == 'SUCCEEDED':
                        continue
                    else:
                        failed_sm_execution_list.append(sm_exec_arn)
                        err_flag = True
                        continue

                if err_flag:
                    return 'FAILED', failed_sm_execution_list
                else:
                    return 'SUCCEEDED', ''
            else:
                self.logger.info("SM Execution List {} is empty, nothing to monitor.".format(self.list_sm_exec_arns))
                return None, []

        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise

    def trigger_stackset_state_machine(self):
        try:
            self.manifest = Manifest(self.manifest_file_path)
            self.start_stackset_sm()
            return
        except Exception as e:
            message = {'FILE': __file__.split('/')[-1], 'METHOD': inspect.stack()[0][3], 'EXCEPTION': str(e)}
            self.logger.exception(message)
            raise
Exemplo n.º 6
0
class SMExecutionManager:
    def __init__(self, logger, sm_input_list):
        self.logger = logger
        self.sm_input_list = sm_input_list
        self.list_sm_exec_arns = []
        self.stack_set_exist = True
        self.solution_metrics = Metrics(logger)
        self.param_handler = CFNParamsHandler(logger)
        self.state_machine = StateMachine(logger)
        self.stack_set = StackSet(logger)
        self.s3 = S3(logger)
        self.wait_time = os.environ.get('WAIT_TIME')
        self.execution_mode = os.environ.get('EXECUTION_MODE')

    def launch_executions(self):
        self.logger.info("%%% Launching State Machine Execution %%%")
        if self.execution_mode.upper() == 'PARALLEL':
            self.logger.info(" | | | | |  Running Parallel Mode. | | | | |")
            return self.run_execution_parallel_mode()

        elif self.execution_mode.upper() == 'SEQUENTIAL':
            self.logger.info(" > > > > >  Running Sequential Mode. > > > > >")
            return self.run_execution_sequential_mode()
        else:
            raise Exception("Invalid execution mode: {}".format(
                self.execution_mode))

    def run_execution_sequential_mode(self):
        status, failed_execution_list = None, []
        # start executions at given intervals
        for sm_input in self.sm_input_list:
            updated_sm_input = self.populate_ssm_params(sm_input)
            stack_set_name = sm_input.get('ResourceProperties')\
                .get('StackSetName', '')

            template_matched, parameters_matched = \
                self.compare_template_and_params(sm_input, stack_set_name)

            self.logger.info("Stack Set Name: {} | "
                             "Same Template?: {} | "
                             "Same Parameters?: {}".format(
                                 stack_set_name, template_matched,
                                 parameters_matched))
            if template_matched and parameters_matched and self.stack_set_exist:
                start_execution_flag = not (
                    self.check_stack_instances_per_account(
                        sm_input, stack_set_name))
            else:
                # the template or parameters needs to be updated - start SM exeution
                start_execution_flag = True

            if start_execution_flag:
                sm_exec_name = self.get_sm_exec_name(updated_sm_input)

                sm_exec_arn = self.setup_execution(updated_sm_input,
                                                   sm_exec_name)
                self.list_sm_exec_arns.append(sm_exec_arn)

                status, failed_execution_list = \
                    self.monitor_state_machines_execution_status()
                if status == 'FAILED':
                    return status, failed_execution_list
                else:
                    self.logger.info("State Machine execution completed. "
                                     "Starting next execution...")
        else:
            self.logger.info("All State Machine executions completed.")
        return status, failed_execution_list

    def run_execution_parallel_mode(self):
        # start executions at given intervals
        for sm_input in self.sm_input_list:
            sm_exec_name = self.get_sm_exec_name(sm_input)
            sm_exec_arn = self.setup_execution(sm_input, sm_exec_name)
            self.list_sm_exec_arns.append(sm_exec_arn)
            time.sleep(int(self.wait_time))
        # monitor execution status
        status, failed_execution_list = \
            self.monitor_state_machines_execution_status()
        return status, failed_execution_list

    @staticmethod
    def get_sm_exec_name(sm_input):
        if os.environ.get('STAGE_NAME').upper() == 'BASELINERESOURCES':
            return sm_input.get('ResourceProperties').get('StackSetName')
        else:
            return str(uuid4())  # return random string

    def setup_execution(self, sm_input, name):
        self.logger.info("State machine Input: {}".format(sm_input))

        # set execution name
        exec_name = "%s-%s-%s" % (sm_input.get('RequestType'),
                                  trim_length_from_end(
                                      name.replace(" ", ""),
                                      50), time.strftime("%Y-%m-%dT%H-%M-%S"))

        # execute all SM at regular interval of wait_time
        return self.state_machine.trigger_state_machine(
            os.environ.get('SM_ARN'), sm_input, exec_name)

    def populate_ssm_params(self, sm_input):
        """The scenario is if you have one CFN resource that exports output
         from CFN stack to SSM parameter and then the next CFN resource
         reads the SSM parameter as input, then it has to wait for the first
         CFN resource to finish; read the SSM parameters and use its value
         as input for second CFN resource's input for SM. Get the parameters
         for CFN template from sm_input
        """
        self.logger.info(
            "Populating SSM parameter values for SM input: {}".format(
                sm_input))
        params = sm_input.get('ResourceProperties')\
            .get('Parameters', {})
        # First transform it from {name: value} to [{'ParameterKey': name},
        # {'ParameterValue': value}]
        # then replace the SSM parameter names with its values
        sm_params = self.param_handler.update_params(transform_params(params))
        # Put it back into the self.state_machine_event
        sm_input.get('ResourceProperties').update({'Parameters': sm_params})
        self.logger.info("Done populating SSM parameter values for SM input:"
                         " {}".format(sm_input))
        return sm_input

    def compare_template_and_params(self, sm_input, stack_name):

        self.logger.info("Comparing the templates and parameters.")
        template_compare, params_compare = False, False
        if stack_name:
            describe_response = self.stack_set\
                .describe_stack_set(stack_name)
            self.logger.info("Print Describe Stack Set Response: {}".format(
                describe_response))
            if describe_response is not None:
                self.logger.info("Found existing stack set.")

                operation_status_flag = self.get_stack_set_operation_status(
                    stack_name)

                if operation_status_flag:
                    self.logger.info("Continuing...")
                else:
                    return operation_status_flag, operation_status_flag

                # Compare template copy - START
                self.logger.info(
                    "Comparing the template of the StackSet:"
                    " {} with local copy of template".format(stack_name))

                template_http_url = sm_input.get('ResourceProperties')\
                    .get('TemplateURL', '')
                if template_http_url:
                    bucket_name, key_name = parse_bucket_key_names(
                        template_http_url)
                    local_template_file = tempfile.mkstemp()[1]
                    self.s3.download_file(bucket_name, key_name,
                                          local_template_file)
                else:
                    self.logger.error("TemplateURL in state machine input "
                                      "is empty. Check state_machine_event"
                                      ":{}".format(sm_input))
                    return False, False

                cfn_template_file = tempfile.mkstemp()[1]
                with open(cfn_template_file, "w") as f:
                    f.write(
                        describe_response.get('StackSet').get('TemplateBody'))
                # cmp function return true of the contents are same
                template_compare = filecmp.cmp(local_template_file,
                                               cfn_template_file, False)
                self.logger.info("Comparing the parameters of the StackSet"
                                 ": {} with local copy of JSON parameters"
                                 " file".format(stack_name))

                params_compare = True
                params = sm_input.get('ResourceProperties')\
                    .get('Parameters', {})
                # template are same - compare parameters (skip if template
                # are not same)
                if template_compare:
                    cfn_params = reverse_transform_params(
                        describe_response.get('StackSet').get('Parameters'))
                    for key, value in params.items():
                        if cfn_params.get(key, '') == value:
                            pass
                        else:
                            params_compare = False
                            break

                self.logger.info(
                    "template_compare={}; params_compare={}".format(
                        template_compare, params_compare))
            else:
                self.logger.info(
                    'Stack Set does not exist. Creating a new stack set ....')
                template_compare, params_compare = True, True
                # set this flag to create the stack set
                self.stack_set_exist = False

        return template_compare, params_compare

    def get_stack_set_operation_status(self, stack_name):
        self.logger.info("Checking the status of last stack set "
                         "operation on {}".format(stack_name))
        response = self.stack_set. \
            list_stack_set_operations(StackSetName=stack_name,
                                      MaxResults=1)
        if response:
            if response.get('Summaries'):
                for instance in response.get('Summaries'):
                    self.logger.info("Status of last stack set "
                                     "operation : {}".format(
                                         instance.get('Status')))
                    if instance.get('Status') != 'SUCCEEDED':
                        self.logger.info(
                            "The last stack operation"
                            " did not succeed. "
                            "Triggering "
                            " Update StackSet for {}".format(stack_name))
                        return False
        return True

    def check_stack_instances_per_account(self, sm_input, stack_name):
        """:return: boolean
        # false: if the SM execution need to make CRUD operations on the StackSet
        # true: if no changes to Stack Set or Stack Instances are required
        """
        flag = False
        account_list = sm_input.get('ResourceProperties') \
            .get("AccountList", [])
        if account_list:
            self.logger.info("Comparing the Stack Instances "
                             "Account & Regions for "
                             "StackSet: {}".format(stack_name))
            expected_region_list = set(
                sm_input.get('ResourceProperties').get("RegionList", []))

            # iterator over accounts in event account list
            for account in account_list:
                actual_region_list = set()

                self.logger.info("### Listing the Stack "
                                 "Instances for StackSet: {}"
                                 " and Account: {} ###".format(
                                     stack_name, account))
                stack_instance_list = self.stack_set. \
                    list_stack_instances_per_account(stack_name,
                                                     account)

                self.logger.info(stack_instance_list)

                if stack_instance_list:
                    for instance in stack_instance_list:
                        if instance.get('Status') \
                                .upper() == 'CURRENT':
                            actual_region_list \
                                .add(instance.get('Region'))
                        else:
                            self.logger.info("Found at least one of"
                                             " the Stack Instances"
                                             " in {} state."
                                             " Triggering Update"
                                             " StackSet for {}".format(
                                                 instance.get('Status'),
                                                 stack_name))
                            return False
                else:
                    self.logger.info("Found no stack instances in"
                                     " account: {},Updating "
                                     "StackSet: {}".format(
                                         account, stack_name))
                    return False

                if expected_region_list. \
                        issubset(actual_region_list):
                    self.logger.info("Found expected regions : {} "
                                     "in deployed stack instances :"
                                     " {}, so skipping Update "
                                     "StackSet for {}".format(
                                         expected_region_list,
                                         actual_region_list, stack_name))
                    flag = True
        else:
            self.logger.info("Found no changes in template "
                             "& parameters, so skipping Update  "
                             "StackSet for {}".format(stack_name))
            flag = True
        return flag

    def monitor_state_machines_execution_status(self):
        if self.list_sm_exec_arns:
            final_status = 'RUNNING'

            while final_status == 'RUNNING':
                for sm_exec_arn in self.list_sm_exec_arns:
                    status = self.state_machine.check_state_machine_status(
                        sm_exec_arn)
                    if status == 'RUNNING':
                        final_status = 'RUNNING'
                        time.sleep(int(self.wait_time))
                        break
                    else:
                        final_status = 'COMPLETED'

            err_flag = False
            failed_sm_execution_list = []
            for sm_exec_arn in self.list_sm_exec_arns:
                status = self.state_machine.check_state_machine_status(
                    sm_exec_arn)
                if status == 'SUCCEEDED':
                    continue
                else:
                    failed_sm_execution_list.append(sm_exec_arn)
                    err_flag = True
                    continue

            if err_flag:
                return 'FAILED', failed_sm_execution_list
            else:
                return 'SUCCEEDED', ''
        else:
            self.logger.info("SM Execution List {} is empty, nothing to "
                             "monitor.".format(self.list_sm_exec_arns))
            return None, []