def handle_stack_event(self, event, valid_from_timestamp, expected_stack_event_status, stack_name): """ Handle stack event. Return it if it has the expected status :param event: raw event :param valid_from_timestamp: earliest timestamp from which the event is considered relevant :param expected_stack_event_status: :param stack_name: the relevant stacks name :return: boto3 stack event if it has expected status | None :raise CfnStackActionFailedException: """ if event["Timestamp"] > valid_from_timestamp: if event["ResourceType"] == "AWS::CloudFormation::Stack" and event[ "LogicalResourceId"] == stack_name: self.logger.debug("Raw event: {0}".format(event)) if event["ResourceStatus"] == expected_stack_event_status: return event if event["ResourceStatus"].endswith("_FAILED"): raise CfnStackActionFailedException( "Stack is in {0} state".format( event["ResourceStatus"])) if event["ResourceStatus"].endswith("ROLLBACK_IN_PROGRESS"): self.logger.error( "Failed to create stack (Reason: {0})".format( event["ResourceStatusReason"])) return None if event["ResourceStatus"].endswith("ROLLBACK_COMPLETE"): raise CfnStackActionFailedException("Rollback occured") else: if event["ResourceStatus"].endswith("_FAILED"): self.logger.error( "Failed to create {0} (Reason: {1})".format( event["LogicalResourceId"], event["ResourceStatusReason"])) return None else: status_reason = event.get("ResourceStatusReason", None) status_reason_string = " ({0})".format( status_reason) if status_reason else "" event_string = "{0} {1}: {2}{3}".format( event["StackName"], event["LogicalResourceId"], event["ResourceStatus"], status_reason_string) self.logger.info(event_string) return None
def wait_for_stack_events(self, stack_name, expected_event, valid_from_timestamp, timeout): self.logger.debug("Waiting for {0} events, newer than {1}".format( expected_event, valid_from_timestamp)) seen_event_ids = [] start = time.time() while time.time() < (start + timeout): events = self.get_stack_events(stack_name) events.reverse() for event in events: if event.event_id not in seen_event_ids: seen_event_ids.append(event.event_id) if event.timestamp > valid_from_timestamp: if event.resource_type == "AWS::CloudFormation::Stack": self.logger.debug(event) if event.resource_status == expected_event: return event if event.resource_status.endswith("_FAILED"): raise CfnStackActionFailedException( "Stack is in {0} state".format( event.resource_status)) if event.resource_status.endswith( "ROLLBACK_IN_PROGRESS"): self.logger.error( "Failed to create stack (Reason: {0})". format(event.resource_status_reason)) if event.resource_status.endswith( "ROLLBACK_COMPLETE"): raise CfnStackActionFailedException( "Rollback occured") else: if event.resource_status.endswith("_FAILED"): self.logger.error( "Failed to create {0} (Reason: {1})". format(event.logical_resource_id, event.resource_status_reason)) else: self.logger.info(event) time.sleep(10) raise CfnStackActionFailedException( "Timeout occurred waiting for '{0}' on stack {1}".format( expected_event, stack_name))
def create_stack(self, stack): self.logger.debug("Creating stack: {0}".format(stack)) assert isinstance(stack, CloudFormationStack) try: stack_parameters_string = get_pretty_parameters_string(stack) self.logger.info( "Creating stack {0} ({1}) with parameters:\n{2}".format( stack.name, stack.template.name, stack_parameters_string)) if self.dry_run: self.logger.info('Dry run. Exiting.') return self._create_stack(stack) self.wait_for_stack_action_to_complete(stack.name, "create", stack.timeout) stack_outputs = get_pretty_stack_outputs( self.get_stack_outputs(stack)) if stack_outputs: self.logger.info( "Create completed for {0} with outputs: \n{1}".format( stack.name, stack_outputs)) else: self.logger.info("Create completed for {0}".format(stack.name)) except (BotoCoreError, ClientError, CfnSphereBotoError) as e: raise CfnStackActionFailedException( "Could not create {0}: {1}".format(stack.name, e))
def update_stack(self, stack): try: try: self._update_stack(stack) except BotoServerError as e: if self.is_boto_no_update_required_exception(e): self.logger.info( "Stack {0} does not need an update".format(stack.name)) return else: self.logger.info( "Updating stack {0} ({1}) with parameters:\n{2}". format(stack.name, stack.template.name, get_pretty_parameters_string(stack.parameters))) raise self.logger.info( "Updating stack {0} ({1}) with parameters:\n{2}".format( stack.name, stack.template.name, get_pretty_parameters_string(stack.parameters))) self.wait_for_stack_action_to_complete(stack.name, "update", stack.timeout) self.logger.info("Update completed for {0}".format(stack.name)) except BotoServerError as e: raise CfnStackActionFailedException( "Could not update {0}: {1}".format(stack.name, e.message))
def execute_change_set(self): print('executing change set') print(self.config.change_set) change_set = self.cfn.get_change_set(self.config.change_set) if change_set is None: raise CfnStackActionFailedException( "Could not execute change set {0}: Does not exist or is in an invalid state." .format(self.config.change_set)) stack_name = self.cfn.get_stack_name_by_arn(change_set['StackId']) stack = self.cfn.get_stack(stack_name) if not self.cfn.change_set_is_executable(change_set): raise CfnStackActionFailedException( "Could not execute change set {0}: Does not exist or is in an invalid state." .format(self.config.change_set)) else: self.cfn.execute_change_set(stack, self.config.change_set)
def create_change_set(self, stack, change_set_type): self.logger.debug("Creating stack changeset: {}".format(stack)) assert isinstance(stack, CloudFormationStack) try: stack_parameters_string = get_pretty_parameters_string(stack) self.logger.info( "Creating stack changeset {0} ({1}) with parameters:\n{2}". format(stack.name, stack.template.name, stack_parameters_string)) self._create_stack_change_set(stack, change_set_type) except (BotoCoreError, ClientError, CfnSphereBotoError) as e: raise CfnStackActionFailedException( "Could not create change set {0}: {1}".format(stack.name, e))
def validate_stack_is_ready_for_action(self, stack): try: cfn_stack = self.get_stack(stack.name) except BotoServerError as e: raise CfnSphereBotoError(e) valid_states = [ "CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE" ] if cfn_stack.stack_status not in valid_states: raise CfnStackActionFailedException( "Stack {0} is in '{1}' state.".format(cfn_stack.stack_name, cfn_stack.stack_status))
def delete_stack(self, stack): try: self.logger.info("Deleting stack {0}".format(stack.name)) self._delete_stack(stack) try: self.wait_for_stack_action_to_complete(stack.name, "delete", 600) except BotoServerError as e: self.logger.info(e) self.logger.info("Deletion completed for {0}".format(stack.name)) except BotoServerError as e: raise CfnStackActionFailedException( "Could not delete {0}: {1}".format(stack.name, e.message))
def create_stack(self, stack): assert isinstance(stack, CloudFormationStack) try: self.logger.info( "Creating stack {0} from template {1} with parameters:\n{2}". format(stack.name, stack.template.name, get_pretty_parameters_string(stack.parameters))) self._create_stack(stack) self.wait_for_stack_action_to_complete(stack.name, "create", stack.timeout) self.logger.info("Create completed for {0}".format(stack.name)) except BotoServerError as e: raise CfnStackActionFailedException( "Could not create {0}: {1}".format(stack.name, e.message))
def update_stack(self, stack): self.logger.debug("Updating stack: {0}".format(stack)) assert isinstance(stack, CloudFormationStack) try: stack_parameters_string = get_pretty_parameters_string(stack) if stack.stack_policy: self.logger.info("Setting stack policy for stack {0}".format( stack.name)) self._set_stack_policy(stack) try: self._update_stack(stack) except ClientError as e: if self.is_boto_no_update_required_exception(e): self.logger.info( "Stack {0} does not need an update".format(stack.name)) return else: self.logger.info( "Updating stack {0} ({1}) with parameters:\n{2}". format(stack.name, stack.template.name, stack_parameters_string)) raise self.logger.info( "Updating stack {0} ({1}) with parameters:\n{2}".format( stack.name, stack.template.name, stack_parameters_string)) self.wait_for_stack_action_to_complete(stack.name, "update", stack.timeout) stack_outputs = get_pretty_stack_outputs( self.get_stack_outputs(stack)) if stack_outputs: self.logger.info( "Update completed for {0} with outputs: \n{1}".format( stack.name, stack_outputs)) else: self.logger.info("Update completed for {0}".format(stack.name)) except (BotoCoreError, ClientError, CfnSphereBotoError) as e: raise CfnStackActionFailedException( "Could not update {0}: {1}".format(stack.name, e))
def validate_stack_is_ready_for_action(self, stack): """ Check if a stack is in a state capable for modification actions :param stack: cfn_sphere.aws.cfn.CloudFormationStack :raise CfnStackActionFailedException: if the stack is in an invalid state """ cfn_stack = self.get_stack(stack.name) valid_states = [ "CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE" ] if cfn_stack.stack_status not in valid_states: raise CfnStackActionFailedException( "Stack {0} is in '{1}' state.".format(cfn_stack.stack_name, cfn_stack.stack_status))
def execute_change_set(self, stack, change_set): self.logger.debug("Executing stack changeset: {}".format(change_set)) try: response = self.client.execute_change_set(ChangeSetName=change_set) self.wait_for_stack_action_to_complete(stack.name, "update", 120) stack_outputs = get_pretty_stack_outputs( self.get_stack_outputs(stack)) if stack_outputs: self.logger.info( "Update completed for {0} with outputs: \n{1}".format( stack.name, stack_outputs)) else: self.logger.info("Update completed for {0}".format(stack.name)) # Blank it out instead of updating the cache as if it's needed again # the code will automatically populate the describe/stacks cache again. self.cached = {STACK_DESCRIPTIONS: None, RESOURCE_ALL_STACKS: None} except (BotoCoreError, ClientError, CfnSphereBotoError) as e: raise CfnStackActionFailedException( "Could not execute {0}: {1}".format(change_set, e))
def delete_stack(self, stack): self.logger.debug("Deleting stack: {0}".format(stack)) assert isinstance(stack, CloudFormationStack) try: self.logger.info("Deleting stack {0}".format(stack.name)) self._delete_stack(stack) try: self.wait_for_stack_action_to_complete(stack.name, "delete", 600) except CfnSphereBotoError as e: if self.is_boto_stack_does_not_exist_exception( e.boto_exception): pass else: raise self.logger.info("Deletion completed for {0}".format(stack.name)) except (BotoCoreError, ClientError, CfnSphereBotoError) as e: raise CfnStackActionFailedException( "Could not delete {0}: {1}".format(stack.name, e))
def wait_for_stack_event(self, stack_name, expected_event_status, valid_from_timestamp, timeout): """ Wait for a new stack event. Return it if it has the expected status :param stack_name: str :param expected_event_status: str :param valid_from_timestamp: timestamp :param timeout: int :return: boto3 stack event :raise CfnStackActionFailedException: """ self.logger.debug("Waiting for {0} events, newer than {1}".format( expected_event_status, valid_from_timestamp)) seen_event_ids = [] start = time.time() while time.time() < (start + timeout): events = self.get_stack_events(stack_name) events.reverse() for event in events: if event["EventId"] not in seen_event_ids: seen_event_ids.append(event["EventId"]) event = self.handle_stack_event(event, valid_from_timestamp, expected_event_status, stack_name) if event: return event time.sleep(10) raise CfnStackActionFailedException( "Timeout occurred waiting for '{0}' on stack {1}".format( expected_event_status, stack_name))