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
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
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
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
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
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
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
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
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
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
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