def deploy(self): """ Create a new Stack or update an existing Stack using a ChangeSet. This will wait for ChangeSet to be created and print the details, and if Auto-Approve is set, will then execute the ChangeSet and wait for that to complete. If there are no changes in the ChangeSet it will be automatically deleted. :raises StackError: If creating the ChangeSet or executing it fails """ if not StackStatus.is_creatable(self.stack) and not StackStatus.is_updatable(self.stack): stack_status = StackStatus.get_status(self.stack) if stack_status == StackStatus.ROLLBACK_COMPLETE: warn(f'Deleting Stack {self.config.stack_name} in ROLLBACK_COMPLETE status before attempting to create') self.delete() else: raise ValidationError(f'Stack {self.config.stack_name} is not in a deployable status: ' f'{stack_status.name}') info(f'\nCreating ChangeSet {self.change_set_name}\n') try: self.client.create_change_set(**self.build_change_set_args()) if self.wait_for_change_set(): if self.auto_approve: self.execute_change_set() else: self.pending_change_set() except ClientError as ce: raise StackError(ce)
def test_is_status(): assert StackStatus.is_status( {STACK_STATUS: StackStatus.CREATE_COMPLETE.name}, StackStatus.CREATE_COMPLETE) assert not StackStatus.is_status( {STACK_STATUS: StackStatus.CREATE_COMPLETE.name}, StackStatus.CREATE_FAILED)
def reject_change_set(self): """ Delete the named ChangeSet, and if the stack is in the REVIEW_IN_PROGRESS status and there are no remaining ChangeSets, then delete the stack. """ if not self.stack: raise ValidationError(f'Stack {self.config.stack_name} not found') self.print_stack_status() try: self.client.describe_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) except self.client.exceptions.ChangeSetNotFoundException: raise ValidationError( f'ChangeSet {self.change_set_name} not found') info( f'\nDeleting ChangeSet {self.change_set_name} for {self.config.stack_name}' ) self.client.delete_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) if StackStatus.is_status(self.stack, StackStatus.REVIEW_IN_PROGRESS): change_sets = self.client.list_change_sets( StackName=self.config.stack_name)['Summaries'] if not change_sets: info( f'\nDeleting REVIEW_IN_PROGRESS Stack {self.config.stack_name} that has no remaining ChangeSets' ) # Delete stack, no need to wait for deletion to complete self.client.delete_stack(StackName=self.config.stack_name)
def delete(self, retain_resources=[]): """ Delete a Stack, optionally retraining certain resources. Waits for Stack to delete and prints Events if deletion fails :param list retain_resources: List of LogicalIds to retain :raises StackError: if deletion fails """ if not self.stack: raise ValidationError(f'Stack {self.config.stack_name} not found') if not StackStatus.is_deletable(self.stack): raise ValidationError(f'Stack {self.config.stack_name} is not in a deletable status: ' f'{StackStatus.get_status(self.stack).name}') info(f'\nDeleting Stack {self.config.stack_name}') last_timestamp = self.get_last_timestamp() try: self.client.delete_stack(StackName=self.config.stack_name, RetainResources=retain_resources) self.client.get_waiter('stack_delete_complete').wait( StackName=self.config.stack_name, WaiterConfig={'Delay': 10, 'MaxAttempts': 360}) info(f'\nDeletion of Stack {self.config.stack_name} successfully completed') self.stack = None except ClientError as ce: raise StackError(ce) except WaiterError as we: error(f'\nDeletion of Stack {self.config.stack_name} failed:\n') self.print_events(last_timestamp) raise StackError(we)
def execute_change_set(self): """ Execute a ChangeSet, waiting for execution to complete and printing the details of Stack Events caused by this ChangeSet :raises StackError: If there is an error executing the ChangeSet """ last_timestamp = self.get_last_timestamp() try: info(f'\nExecuting ChangeSet {self.change_set_name} for {self.config.stack_name}') self.client.execute_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) waiter_name = 'stack_create_complete' if StackStatus.is_creatable(self.stack) else 'stack_update_complete' self.client.get_waiter(waiter_name).wait(StackName=self.config.stack_name, WaiterConfig={'Delay': 10, 'MaxAttempts': 360}) info(f'\nChangeSet {self.change_set_name} for {self.config.stack_name} successfully completed:\n') self.print_events(last_timestamp) except ClientError as ce: raise StackError(ce) except WaiterError as we: error(f'\nChangeSet {self.change_set_name} for {self.config.stack_name} failed:\n') self.print_events(last_timestamp) raise StackError(we)
def build_change_set_args(self): """ Build dictionary of arguments for creating a ChangeSet :return: Dictionary of arguments based upon Config """ args = { 'StackName': self.config.stack_name, 'ChangeSetName': self.change_set_name, 'ChangeSetType': 'CREATE' if StackStatus.is_creatable(self.stack) else 'UPDATE', 'Parameters': self.build_parameters(), 'Tags': self.build_tags() } if self.config.capabilities: args['Capabilities'] = self.config.capabilities if Config.is_template_url(self.config.template): args['TemplateURL'] = self.config.template else: with open(self.config.template) as t: args['TemplateBody'] = t.read() return args
def test_is_creatable(): assert StackStatus.is_creatable(None) assert StackStatus.is_creatable( {STACK_STATUS: StackStatus.REVIEW_IN_PROGRESS.name}) assert not StackStatus.is_creatable( {STACK_STATUS: StackStatus.CREATE_COMPLETE.name})
def test_is_status_invalid_status(): with pytest.raises(ValueError, match='Unknown StackStatus UNKNOWN'): StackStatus.is_status({STACK_STATUS: 'UNKNOWN'}, StackStatus.CREATE_COMPLETE)
def test_is_status_missing_status(): with pytest.raises(ValueError, match='No StackStatus available'): StackStatus.is_status({}, StackStatus.CREATE_COMPLETE)