Exemple #1
0
    def create_stack_set(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        # Create a new stack set
        stack_set = StackSet(self.logger)
        self.logger.info("Creating StackSet")
        parameters = self._get_ssm_secure_string(self.params.get('Parameters'))
        response = stack_set.create_stack_set(self.params.get('StackSetName'),
                                              self.params.get('TemplateURL'),
                                              parameters,
                                              self.params.get('Capabilities'),
                                              'AWS_Solutions',
                                              'CustomControlTowerStackSet')
        if response.get('StackSetId') is not None:
            value = "success"
        else:
            value = "failure"
        self.event.update({'StackSetStatus': value})
        # set create stack instance flag to yes (Handle SM Condition:
        # Create or Delete Stack Instance?)
        # check if the account list is empty
        create_flag = 'no' if not self.params.get('AccountList') else 'yes'
        self.event.update({'CreateInstance': create_flag})
        # set delete stack instance flag to no (Handle SM Condition:
        # Delete Stack Instance or Finish?)
        self.event.update({'DeleteInstance': 'no'})
        return self.event
Exemple #2
0
    def describe_stack_set(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        # add loop flag to handle Skip StackSet Update choice
        if self.event.get('LoopFlag') is None:
            self.event.update({'LoopFlag': 'not-applicable'})

        # To prevent CFN from throwing 'Response object is too long.'
        # when the event payload gets overloaded Deleting the
        # 'OldResourceProperties' from event, since it not being used in
        # the SM

        if self.event.get('OldResourceProperties'):
            self.event.pop('OldResourceProperties', '')

        # Check if stack set already exist
        stack_set = StackSet(self.logger)
        response = stack_set.describe_stack_set(
            self.params.get('StackSetName'))
        self.logger.info("Describe Response")
        self.logger.info(response)
        # If stack_set already exist, skip to create the stack_set_instance
        if response is not None:
            value = "yes"
            self.logger.info("Found existing stack set.")
        else:
            value = "no"
            self.logger.info("Existing stack set not found.")
        self.event.update({'StackSetExist': value})
        return self.event
Exemple #3
0
 def delete_stack_set(self):
     self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                      inspect.stack()[0][3])
     self.logger.info(self.params)
     # Delete StackSet
     stack_set = StackSet(self.logger)
     self.logger.info("Deleting StackSet: {}".format(
         self.params.get('StackSetName')))
     self.logger.info(
         stack_set.delete_stack_set(self.params.get('StackSetName')))
     return self.event
 def __init__(self, logger, sm_input_list, enforce_successful_stack_instances=False):
     self.logger = logger
     self.sm_input_list = sm_input_list
     self.list_sm_exec_arns = []
     self.stack_set_exist = True
     self.solution_metrics = SolutionMetrics(logger)
     self.param_handler = CFNParamsHandler(logger)
     self.state_machine = StateMachine(logger)
     self.stack_set = StackSet(logger)
     self.wait_time = os.environ.get('WAIT_TIME')
     self.execution_mode = os.environ.get('EXECUTION_MODE')
     self.enforce_successful_stack_instances = enforce_successful_stack_instances
Exemple #5
0
    def describe_stack_set_operation(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)
        self.event.update({'RetryDeleteFlag': False})

        stack_set = StackSet(self.logger)
        response = stack_set.describe_stack_set_operation(
            self.params.get('StackSetName'), self.event.get('OperationId'))
        self.logger.info(response)
        operation_status = response.get('StackSetOperation', {}).get('Status')
        self.logger.info("Operation Status: {}".format(operation_status))
        if operation_status == 'FAILED':
            account_id = self.params.get('AccountList')[0] \
                            if type(self.params.get('AccountList')) is list \
                            else None
            if account_id:
                for region in self.params.get('RegionList'):
                    self.logger.info("Account: {} - describing stack "
                                     "instance in {} region".format(
                                         account_id, region))
                    try:
                        resp = stack_set.describe_stack_instance(
                            self.params.get('StackSetName'), account_id,
                            region)
                        self.event.update({
                            region:
                            resp.get('StackInstance', {}).get('StatusReason')
                        })
                    except ClientError as e:
                        # When CFN has triggered StackInstance delete and
                        # the SCP is still attached (due to race condition)
                        # , then it fails to delete the stack and StackSet
                        # throws the StackInstanceNotFoundException
                        # exception back, the CFN stack in target account
                        # ends up with 'DELETE_FAILED' state
                        # so it should try again
                        if e.response['Error']['Code'] ==  \
                            'StackInstanceNotFoundException' and \
                                self.event.get('RequestType') == 'Delete':
                            self.logger.exception(
                                "Caught exception"
                                "'StackInstanceNotFoundException',"
                                "sending the flag to go back to "
                                " Delete Stack Instances stage...")
                            self.event.update({'RetryDeleteFlag': True})

        operation_status = response.get('StackSetOperation', {}).get('Status')
        self.event.update({'OperationStatus': operation_status})
        return self.event
Exemple #6
0
    def list_stack_instances_account_ids(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        if self.event.get('NextToken') is None \
                or self.event.get('NextToken') == 'Complete':
            accounts = []
        else:
            accounts = self.event.get('StackInstanceAccountList', [])

        # Check if stack instances exist
        stack_set = StackSet(self.logger)
        if self.event.get('NextToken') is not None and \
                self.event.get('NextToken') != 'Complete':
            response = stack_set.list_stack_instances(
                StackSetName=self.params.get('StackSetName'),
                MaxResults=20,
                NextToken=self.event.get('NextToken'))
        else:
            response = stack_set.list_stack_instances(
                StackSetName=self.params.get('StackSetName'), MaxResults=20)

        self.logger.info("List SI Accounts Response")
        self.logger.info(response)

        if response:
            if not response.get('Summaries'):  # 'True' if list is empty
                self.event.update({'NextToken': 'Complete'})
                self.logger.info("No existing stack instances found."
                                 " (Summaries List: Empty)")
            else:
                for instance in response.get('Summaries'):
                    account_id = instance.get('Account')
                    accounts.append(account_id)
                self.event.update(
                    {'StackInstanceAccountList': list(set(accounts))})
                self.logger.info("Next Token Returned: {}".format(
                    response.get('NextToken')))

                if response.get('NextToken') is None:
                    self.event.update({'NextToken': 'Complete'})
                    self.logger.info("No existing stack instances found."
                                     " (Summaries List: Empty)")
                else:
                    self.event.update({'NextToken': response.get('NextToken')})
        return self.event
Exemple #7
0
    def delete_stack_instances(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        # set to default values (new instance creation)
        account_list = self.params.get('AccountList')
        # full region list
        region_list = self.event.get('ExistingRegionList')

        # if DeleteAccountList is not empty
        if self.event.get('DeleteAccountList') is not None and len(
                self.event.get('DeleteAccountList')) != 0:
            account_list = self.event.get('DeleteAccountList')
            # full region list
            region_list = self.event.get('ExistingRegionList')

        # if DeleteRegionList is not empty
        if self.event.get('DeleteRegionList') is not None and len(
                self.event.get('DeleteRegionList')) != 0:
            region_list = self.event.get('DeleteRegionList')

        # both  DeleteAccountList and DeleteRegionList is not empty
        if self.event.get('LoopFlag') == 'yes':
            # delete stack instance in deleted account with all regions
            # stack instances in all regions for existing accounts
            # will be deletion in the second round
            account_list = self.event.get('DeleteAccountList')
            # full region list
            region_list = self.event.get('ExistingRegionList')

        self.event.update({'ActiveAccountList': account_list})
        self.event.update({'ActiveRegionList': region_list})

        # Delete stack_set_instance(s)
        stack_set = StackSet(self.logger)
        self.logger.info("Deleting Stack Instance: {}".format(
            self.params.get('StackSetName')))

        response = stack_set.delete_stack_instances(
            self.params.get('StackSetName'), account_list, region_list)
        self.logger.info(response)
        self.logger.info("Operation ID: {}".format(
            response.get('OperationId')))
        self.event.update({'OperationId': response.get('OperationId')})
        return self.event
Exemple #8
0
    def export_cfn_output(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        regions = self.params.get('RegionList')
        accounts = self.params.get('AccountList')
        stack_set_name = self.params.get('StackSetName')
        stack_set = StackSet(self.logger)

        if len(accounts) == 0 or len(regions) == 0:
            self.logger.info("Either AccountList or RegionList empty; so "
                             "skipping the export_cfn_output ")
            return self.event

        self.logger.info("Picking the first account from AccountList")
        account = accounts[0]

        self.logger.info("Picking the first region from RegionList")
        region = regions[0]

        # First retrieve the Stack ID from the target account,
        # region deployed via the StackSet
        response = stack_set.describe_stack_instance(stack_set_name, account,
                                                     region)

        stack_id, stack_name = self._retrieve_stack_info(
            response, stack_set_name, account, region)

        # instantiate STS class
        _assume_role = AssumeRole()
        cfn = Stacks(self.logger,
                     region,
                     credentials=_assume_role(self.logger, account))
        response = cfn.describe_stacks(stack_id)
        stacks = response.get('Stacks')

        if stacks is not None and type(stacks) is list:
            for stack in stacks:
                self._update_event_with_stack_output(stack, stack_id, account,
                                                     region)
        return self.event
Exemple #9
0
    def update_stack_instances(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        stack_set = StackSet(self.logger)
        # this should come from the event
        override_parameters = self.params.get('ParameterOverrides')
        self.logger.info("override_params_list={}".format(override_parameters))

        response = stack_set.update_stack_instances(
            self.params.get('StackSetName'), self.params.get('AccountList'),
            self.params.get('RegionList'), override_parameters)
        self.logger.info("Update Stack Instance Response")
        self.logger.info(response)
        self.logger.info("Operation ID: {}".format(
            response.get('OperationId')))
        self.event.update({'OperationId': response.get('OperationId')})
        # need for Delete Stack Instance or Finish? choice in the
        # state machine. No will route to Finish path.
        self.event.update({'DeleteInstance': 'no'})
        return self.event
Exemple #10
0
    def update_stack_set(self):
        # Updates the stack set and all associated stack instances.
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)
        stack_set = StackSet(self.logger)

        # Update existing StackSet
        self.logger.info("Updating Stack Set: {}".format(
            self.params.get('StackSetName')))

        parameters = self._get_ssm_secure_string(self.params.get('Parameters'))
        response = stack_set.update_stack_set(self.params.get('StackSetName'),
                                              parameters,
                                              self.params.get('TemplateURL'),
                                              self.params.get('Capabilities'))

        self.logger.info("Response Update Stack Set")
        self.logger.info(response)
        self.logger.info("Operation ID: {}".format(
            response.get('OperationId')))
        self.event.update({'OperationId': response.get('OperationId')})
        return self.event
 def __init__(self):
     self.logger = logger
     self.stack_set = StackSet(logger)
     self.control_tower_baseline_config_stackset = os.environ['CONTROL_TOWER_BASELINE_CONFIG_STACKSET'] \
         if os.getenv('CONTROL_TOWER_BASELINE_CONFIG_STACKSET') is not None else 'AWSControlTowerBP-BASELINE-CONFIG'
class OrganizationsData:
    """
    This class build organization details including active accounts under
    an OU, account to OU mapping, OU name to OU id mapping, account name to
    account id mapping, etc.
    """

    def __init__(self):
        self.logger = logger
        self.stack_set = StackSet(logger)
        self.control_tower_baseline_config_stackset = os.environ['CONTROL_TOWER_BASELINE_CONFIG_STACKSET'] \
            if os.getenv('CONTROL_TOWER_BASELINE_CONFIG_STACKSET') is not None else 'AWSControlTowerBP-BASELINE-CONFIG'

    def get_accounts_in_ou(self, ou_id_to_account_map, ou_name_to_id_map, ou_list):
        accounts_in_ou = []
        ou_ids_manifest = []
        accounts_in_nested_ou = []

        if 'Root' in ou_list:
            accounts_list, region_list = self.get_accounts_in_ct_baseline_config_stack_set()
            accounts_in_ou = accounts_list
        else: 
            # convert OU Name to OU IDs
            for ou_name in ou_list:
                if ':' in ou_name: # Process nested OU. For example: TestOU1:TestOU2:TestOU3
                    ou_id = self.get_ou_id(ou_name, ":")
                    accounts_in_nested_ou.extend(self.get_active_accounts_in_ou(ou_id))
                    self.logger.debug("[manifest_parser.get_accounts_in_ou] ou_name: {}; ou_id: {}; accounts_in_nested_ou: {}" \
                                        .format(ou_name, ou_id, accounts_in_nested_ou))
                else:
                    ou_id = [value for key, value in ou_name_to_id_map.items()
                            if ou_name == key]
                    ou_ids_manifest.extend(ou_id)
                    self.logger.debug("[manifest_parser.get_accounts_in_ou] ou_name: {}; ou_id: {}; ou_ids_manifest for non-nested ous: {}" \
                                        .format(ou_name, ou_id, ou_ids_manifest))

            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.debug("[manifest_parser.get_accounts_in_ou] Accounts in non_nested OUs: {}" \
                            .format(accounts_in_ou))

            self.logger.debug("[manifest_parser.get_accounts_in_ou] Accounts in nested OUs: {}" \
                            .format(accounts_in_nested_ou))

            # add accounts for nested ous 
            accounts_in_ou.extend(accounts_in_nested_ou)

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

        return accounts_in_ou

    def get_final_account_list(self, account_list, accounts_in_all_ous,
                               accounts_in_ou, name_to_account_map):
        # separate account id and emails
        name_list = []
        new_account_list = []
        self.logger.info(account_list)
        for item in account_list:
            # if an actual account ID
            if item.isdigit() and len(item) == 12:
                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() == key.lower()]
                self.logger.info(f"==== name_account: {name_account}")
                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)
        # remove duplicate accounts
        return list(set(sanitized_account_list))

    def get_organization_details(self) -> dict:
        """
        Return:
            dict with following properties:
            accounts_in_all_ous: list. Active accounts
            ou_id_to_account_map: dictionary. Accounts for each OU at the root
                                  level
            ou_name_to_id_map: dictionary. OU Name to OU ID mapping
            name_to_account_map: dictionary. account names in manifest to
                                 account ID mapping
        """

        # Returns 1) OU Name to OU ID mapping (dict)
        # key: OU Name (in the manifest); value: OU ID (at root level)
        # 2) all OU IDs under root (dict)
        org = Organizations(self.logger)
        all_ou_ids, ou_name_to_id_map = self._get_ou_ids(org)

        # Returns 1) active accounts (list) under an OU.
        # use case: used to validate accounts in the manifest file
        # 2) Accounts for each OU at the root level.
        # use case: map OU Name to account IDs
        # key: OU ID (str); value: Active accounts (list)
        accounts_in_all_ous, ou_id_to_account_map = \
            self._get_accounts_in_ou(org, all_ou_ids)

        # Returns account name in manifest to account id mapping.
        # key: account name; value: account id
        name_to_account_map, active_account_list = self.get_account_for_name(org)

        # Get all accounts in all ous/nested ous and master account
        accounts_in_all_nested_ous = self.get_all_accounts_in_all_nested_ous()

        return {
            "AccountsInAllOUs": accounts_in_all_ous,
            "OuIdToAccountMap": ou_id_to_account_map,
            "OuNameToIdMap": ou_name_to_id_map,
            "NameToAccountMap": name_to_account_map,
            "ActiveAccountsForRoot": active_account_list,
            "AccountsInAllNestedOUs": accounts_in_all_nested_ous
        }

    def _get_ou_ids(self, org):
        """Get list of accounts under each OU
        :param
        org: Organization service client
        return:
        _all_ou_ids: OU IDs of the OUs in the Organization at the root level
        _ou_name_to_id_map: Account name to account id mapping
        """

        # 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_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 _all_ou_ids, _ou_name_to_id_map

    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_account_for_name(self, org):
        # get all accounts in the organization
        account_list = org.get_accounts_in_org()
        active_account_list = []

        _name_to_account_map = {}
        for account in account_list:
            if account.get("Status") == "ACTIVE":
                active_account_list.append(account.get('Id'))
                _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, active_account_list

    def get_final_ou_list(self, ou_list):
        # Get ou id given an ou name
        final_ou_list = []
        for ou_name in ou_list: 
            ou_id= self.get_ou_id(ou_name, ":")
            this_ou_list= [ou_name, ou_id]
            final_ou_list.append(this_ou_list)
        
        self.logger.info(
            "[manifest_parser.get_final_ou_list] final_ou_list: {} ".format(
                final_ou_list))

        return final_ou_list

    def get_ou_id(self, nested_ou_name, delimiter):
        org = Organizations(self.logger)
        response = org.list_roots()
        root_id = response['Roots'][0].get('Id')
        self.logger.info("[manifest_parser.get_ou_id] Organizations Root Id: {}".format(root_id))

        if nested_ou_name == 'Root':
            return root_id
        else:
            self.logger.info("[manifest_parser.get_ou_id] Looking up the OU Id for OUName: {} with nested"
                                " ou delimiter: '{}'".format(nested_ou_name,
                                                             delimiter))
            ou_id = self._get_ou_id(org, root_id, nested_ou_name, delimiter)
            if ou_id is None or len(ou_id) == 0:
                raise ValueError("OU id is not found for {}".format(nested_ou_name))
        
            return ou_id

    def _get_ou_id(self, org, parent_id, nested_ou_name, delimiter):
        nested_ou_name_list = empty_separator_handler(
            delimiter, nested_ou_name)
        response = self.list_ou_for_parent(
            org, parent_id, list_sanitizer(nested_ou_name_list))
        self.logger.info("[manifest_parser._get_ou_id] _list_ou_for_parent response: {}".format(response))
        return response
    
    def list_ou_for_parent(self, org, parent_id, nested_ou_name_list):
        ou_list = org.list_organizational_units_for_parent(parent_id)
        index = 0  # always process the first item

        self.logger.debug("[manifest_parser.list_ou_id_for_parent] nested_ou_name_list: {}"
                            .format(nested_ou_name_list))
        self.logger.debug("[manifest_parser.list_ou_id_for_parent] ou_list: {} for parent id {}"
                            .format(ou_list, parent_id))

        for dictionary in ou_list:
            self.logger.debug("[manifest_parser.list_ou_id_for_parent] dictionary:{}".format(dictionary))
            if dictionary.get('Name') == nested_ou_name_list[index]:
                self.logger.info("[manifest_parser.list_ou_id_for_parent] OU Name: {} exists under parent id: {}"
                                    .format(dictionary.get('Name'),
                                            parent_id))
                # pop the first item in the list
                nested_ou_name_list.pop(index)
                if len(nested_ou_name_list) == 0:
                    self.logger.info("[manifest_parser.list_ou_id_for_parent] Returning last level OU ID: {}"
                                        .format(dictionary.get('Id')))
                    return dictionary.get('Id')
                else:
                    return self.list_ou_for_parent(org,
                                                    dictionary.get('Id'),
                                                    nested_ou_name_list)

    def get_active_accounts_in_ou(self, ou_id):
        """
        This function gets active accounts in an ou given an ou_id
        """
        org = Organizations(self.logger)
        active_accounts_in_ou = []
        account_list = org.list_accounts_for_parent(ou_id)
        for account in account_list:
            # filter ACTIVE and CREATED accounts
            if account.get('Status') == "ACTIVE":
                active_accounts_in_ou.append(account.get('Id'))

        self.logger.info("All active accounts in nested OU %s:" %(ou_id))
        self.logger.info(active_accounts_in_ou)
    
        return active_accounts_in_ou

    def get_accounts_in_ct_baseline_config_stack_set(self):
        """
        This function gets active accounts which the control tower baseline config stackset deploys to
        """
        accounts_list, region_list = self.stack_set.get_accounts_and_regions_per_stack_set(self.control_tower_baseline_config_stackset)

        self.logger.info("[manifest_parser.get_accounts_in_ct_baseline_config_stack_set] All active accounts in control tower baseline config stackset: {}".format(accounts_list))
        self.logger.info("[manifest_parser.get_accounts_in_ct_baseline_config_stack_set] All regions in control tower baseline stackset: {}".format(region_list))

        return accounts_list, region_list

    def get_master_account_id_in_org(self):
        """
        This function gets master account id for the organization which the user's account belongs to
        """
        org = Organizations(self.logger)
        response = org.describe_organization()
        master_account_id = response['Organization'].get('MasterAccountId')

        self.logger.info("[manifest_parser.get_master_account_id_in_org] Master account id: %s" %(master_account_id))

        return master_account_id

    def get_all_accounts_in_all_nested_ous(self):
        """
        This function gets master account id and all the accounts in all ous (including nested ous)
        """
        accounts_list, region_list = self.get_accounts_in_ct_baseline_config_stack_set()
        master_account_id = self.get_master_account_id_in_org()

        accounts_list.append(master_account_id)

        self.logger.info("[manifest_parser.get_all_accounts_in_all_ous] All active accounts in control tower baseline config stackset plus master account: {}".format(accounts_list))

        return accounts_list
Exemple #13
0
    def create_stack_instances(self):
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        # Create stack instances
        stack_set = StackSet(self.logger)
        # set to default values (new instance creation)
        account_list = self.params.get('AccountList')
        region_list = self.params.get('RegionList')

        # if AddAccountList is not empty
        if self.event.get('AddAccountList') is not None and len(
                self.event.get('AddAccountList')) != 0:
            account_list = self.event.get('AddAccountList')

        # if AddRegionList is not empty
        if self.event.get('AddRegionList') is not None and len(
                self.event.get('AddRegionList')) != 0:
            region_list = self.event.get('AddRegionList')

        # both AddAccountList and AddRegionList are not empty
        if self.event.get('LoopFlag') == 'yes':
            # create new stack instance in new account only with
            # all regions. new stack instances in new region
            # for existing accounts will be deployed in the second round
            if self.event.get('ActiveAccountList') is not None:
                if self.event.get('ActiveAccountList')  \
                        == self.event.get('AddAccountList'):
                    account_list = \
                        self._add_list(self.params.get('AccountList'),
                                       self.event.get('ActiveAccountList'))
            else:
                account_list = self.event.get('AddAccountList')
                region_list = self.params.get('RegionList')

        self.event.update({'ActiveAccountList': account_list})
        self.event.update({'ActiveRegionList': region_list})

        self.logger.info("LoopFlag: {}".format(self.event.get('LoopFlag')))
        self.logger.info(
            "Create stack instances for accounts: {}".format(account_list))
        self.logger.info(
            "Create stack instances in regions:  {}".format(region_list))

        self.logger.info("Creating StackSet Instance: {}".format(
            self.params.get('StackSetName')))
        if 'ParameterOverrides' in self.params:
            self.logger.info("Found 'ParameterOverrides' key in the event.")
            parameters = self._get_ssm_secure_string(
                self.params.get('ParameterOverrides'))
            response = stack_set. \
                create_stack_instances_with_override_params(
                    self.params.get('StackSetName'),
                    account_list,
                    region_list,
                    parameters)
        else:
            response = stack_set.create_stack_instances(
                self.params.get('StackSetName'), account_list, region_list)
        self.logger.info(response)
        self.logger.info("Operation ID: {}".format(
            response.get('OperationId')))
        self.event.update({'OperationId': response.get('OperationId')})
        return self.event
Exemple #14
0
    def list_stack_instances(self):
        """Set values for AccountList, RegionList, LoopFlag, etc.
           that will be used by step functions as input to determine
           its operations: create, update or delete stackset or
           stack instances

        Returns:
            event

        Raises:
        """
        self.logger.info("Executing: " + self.__class__.__name__ + "/" +
                         inspect.stack()[0][3])
        self.logger.info(self.params)

        if 'ParameterOverrides' in self.params.keys():
            self.logger.info("Override parameters found in the event")
            self.event.update({'OverrideParametersExist': 'yes'})
        else:
            self.logger.info("Override parameters NOT found in the event")
            self.event.update({'OverrideParametersExist': 'no'})

        # Check if stack instances exist
        stack_set = StackSet(self.logger)

        # if account list is not present then only create StackSet
        # and skip stack instance creation
        if type(self.params.get('AccountList')) is not list or \
                not self.params.get('AccountList'):
            self._set_skip_stack_instance_operation()
            return self.event
        else:  # proceed if account list exists
            account_id = self.params.get('AccountList')[0]

            # if this is 2nd round, fetch one of the existing accounts
            # that hasn't been processed in the first round
            if self.event.get('ActiveAccountList') is not None  \
                and self.event.get('ActiveRegionList') is not None  \
                and self.params.get('AccountList') !=  \
                    self.event.get('ActiveAccountList'):
                account_id = self._add_list(
                    self.params.get('AccountList'),
                    self.event.get('ActiveAccountList'))[0]

            self.logger.info(
                "Account Id for list stack instance: {}".format(account_id))

            if self.event.get('NextToken') is not None and  \
                    self.event.get('NextToken') != 'Complete':

                self.logger.info('Found next token')
                response = stack_set.list_stack_instances(
                    StackSetName=self.params.get('StackSetName'),
                    StackInstanceAccount=account_id,
                    MaxResults=20,
                    NextToken=self.event.get('NextToken'))
            else:
                self.logger.info('Next token not found.')
                response = stack_set.list_stack_instances(
                    StackSetName=self.params.get('StackSetName'),
                    StackInstanceAccount=account_id,
                    MaxResults=20)
            self.logger.info("List Stack Instance Response"
                             " for account: {}".format(account_id))
            self.logger.info(response)

            if response is not None:
                # If no stack instances are found for new accounts
                # in manifest file entered by user AND no other
                # existing stack instances, then only create stack
                # instance operation is needed.
                # Therefore here set values as input for step functions
                # to trigger create operation accordingly.
                if not response.get('Summaries') and \
                        self.event.get('StackInstanceAccountList') is None:

                    self._set_only_create_stack_instance_operation()
                    return self.event

                # If there are stack instances, follow the route below
                # to determine what operations (create, update, delete)
                # that step functions should perform.
                else:
                    existing_region_list = [] \
                        if self.event.get('ExistingRegionList') is None \
                        else self.event.get('ExistingRegionList')
                    existing_account_list = [] \
                        if self.event.get('StackInstanceAccountList') \
                        is None \
                        else self.event.get('StackInstanceAccountList')

                    if response.get('Summaries'):
                        self.logger.info("Found existing stack instance for "
                                         "AccountList.")
                        self.event.update({'InstanceExist': 'yes'})
                        existing_region_list = \
                            self._get_existing_stack_instance_info(
                                    response.get('Summaries'),
                                    existing_region_list)
                    # If there are no stack instances for new account list
                    # but there are some for existing accounts that are
                    # not in the new account list, get the info about
                    # those stack instances.
                    elif self.event.get('StackInstanceAccountList') \
                            is not None and len(existing_region_list) == 0:
                        account_id = self.event.get(
                            'StackInstanceAccountList')[0]
                        response = stack_set.list_stack_instances(
                            StackSetName=self.params.get('StackSetName'),
                            StackInstanceAccount=account_id,
                            MaxResults=20)
                        self.logger.info("List Stack Instance Response for"
                                         " StackInstanceAccountList")
                        self.logger.info(response)

                        if response.get('Summaries'):
                            self.logger.info("Found existing stack instances "
                                             "for StackInstanceAccountList.")
                            self.event.update({'InstanceExist': 'yes'})
                            existing_region_list =  \
                                self._get_existing_stack_instance_info(
                                        response.get('Summaries'),
                                        existing_region_list)
                        else:
                            existing_region_list =  \
                                self.params.get('RegionList')

                    self.logger.info("Updated existing region List: {}".format(
                        existing_region_list))

                    self.logger.info("Next Token Returned: {}".format(
                        response.get('NextToken')))

                    if response.get('NextToken') is None:

                        add_region_list, delete_region_list, add_account_list,\
                            delete_account_list = \
                            self._get_add_delete_region_account_list(
                                existing_region_list,
                                existing_account_list)
                        self._set_loop_flag(add_region_list,
                                            delete_region_list,
                                            add_account_list,
                                            delete_account_list)
                        self._update_event_for_add(add_account_list,
                                                   add_region_list)
                        self._update_event_for_delete(delete_account_list,
                                                      delete_region_list)
                        self.event.update(
                            {'ExistingRegionList': existing_region_list})
                    else:
                        self.event.update(
                            {'NextToken': response.get('NextToken')})
                        # Update the self.event with existing_region_list
                        self.event.update(
                            {'ExistingRegionList': existing_region_list})
                    return self.event
        return self.event
class SMExecutionManager:
    def __init__(self, logger, sm_input_list, enforce_successful_stack_instances=False):
        self.logger = logger
        self.sm_input_list = sm_input_list
        self.list_sm_exec_arns = []
        self.stack_set_exist = True
        self.solution_metrics = SolutionMetrics(logger)
        self.param_handler = CFNParamsHandler(logger)
        self.state_machine = StateMachine(logger)
        self.stack_set = StackSet(logger)
        self.wait_time = os.environ.get('WAIT_TIME')
        self.execution_mode = os.environ.get('EXECUTION_MODE')
        self.enforce_successful_stack_instances = enforce_successful_stack_instances

    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 ValueError("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 = self.compare_stack_instances(
                    sm_input,
                    stack_set_name
                )
                # template and parameter does not require update
                updated_sm_input.update({'SkipUpdateStackSet': 'yes'})
            else:
                # the template or parameters needs to be updated
                # start SM execution
                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
                elif self.enforce_successful_stack_instances:
                    self.enforce_stack_set_deployment_successful(stack_set_name)

                else:
                    self.logger.info("State Machine execution completed. "
                                     "Starting next execution...")
        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() == 'SCP':
            return sm_input.get('ResourceProperties')\
                .get('PolicyDocument').get('Name')
        elif os.environ.get('STAGE_NAME').upper() == 'STACKSET':
            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.start_execution(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, region = parse_bucket_key_names(
                        template_http_url
                    )
                    local_template_file = tempfile.mkstemp()[1]

                    s3_endpoint_url = "https://s3.%s.amazonaws.com" % region
                    s3 = S3(self.logger,
                            region=region,
                            endpoint_url=s3_endpoint_url)
                    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:
                            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 and 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 compare_stack_instances(self, sm_input: dict, stack_name: str) -> bool:
        """
            Compares deployed stack instances with expected accounts
            & regions for a given StackSet
        :param sm_input: state machine input
        :param stack_name: stack set name
        :return: boolean
        # True: if the SM execution needs to make CRUD operations
         on the StackSet
        # False: if no changes to stack instances are required
        """
        self.logger.info("Comparing deployed stack instances with "
                         "expected accounts & regions for "
                         "StackSet: {}".format(stack_name))
        expected_account_list = sm_input.get('ResourceProperties')\
            .get("AccountList", [])
        expected_region_list = sm_input.get('ResourceProperties')\
            .get("RegionList", [])

        actual_account_list, actual_region_list = \
            self.stack_set.get_accounts_and_regions_per_stack_set(stack_name)

        self.logger.info("*** Stack instances expected to be deployed "
                         "in following accounts. ***")
        self.logger.info(expected_account_list)
        self.logger.info("*** Stack instances actually deployed "
                         "in following accounts. ***")
        self.logger.info(actual_account_list)
        self.logger.info("*** Stack instances expected to be deployed "
                         "in following regions. ***")
        self.logger.info(expected_region_list)
        self.logger.info("*** Stack instances actually deployed "
                         "in following regions. ***")
        self.logger.info(actual_region_list)

        self.logger.info("*** Comparing account lists ***")
        accounts_matched = compare_lists(actual_account_list,
                                         expected_account_list)
        self.logger.info("*** Comparing region lists ***")
        regions_matched = compare_lists(actual_region_list,
                                        expected_region_list,)
        if accounts_matched and regions_matched:
            self.logger.info("No need to add or remove stack instances.")
            return False
        else:
            self.logger.info("Stack instance(s) creation or deletion needed.")
            return True

    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

            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, []

    def enforce_stack_set_deployment_successful(self, stack_set_name: str) -> None:
        failed_detailed_statuses = [
            "CANCELLED",
            "FAILED",
            "INOPERABLE"
        ]
        list_filters = [{"Name": "DETAILED_STATUS", "Values": status} for status in failed_detailed_statuses]
        # Note that we don't paginate because if this API returns any elements, failed instances exist.
        for list_filter in list_filters:
            response = self.stack_set.list_stack_instances(StackSetName=stack_set_name, Filters=[list_filter])
            if response.get("Summaries", []):
                raise StackSetHasFailedInstances(stack_set_name=stack_set_name, failed_stack_set_instances=response["Summaries"])
        return None