Пример #1
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
Пример #2
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