Beispiel #1
0
def get_stack_events(cfn, stack_name, events_limit, token_filter=None):
    '''This event data was never correct, it worked as a side effect. So the v2.3 format is different.'''
    ret = {'events': [], 'log': []}

    try:
        pg = cfn.get_paginator('describe_stack_events').paginate(
            StackName=stack_name, PaginationConfig={'MaxItems': events_limit})
        if token_filter is not None:
            events = list(
                pg.search("StackEvents[?ClientRequestToken == '{0}']".format(
                    token_filter)))
        else:
            events = list(pg.search("StackEvents[*]"))
    except (botocore.exceptions.ValidationError,
            botocore.exceptions.ClientError) as err:
        error_msg = boto_exception(err)
        if 'does not exist' in error_msg:
            # missing stack, don't bail.
            ret['log'].append('Stack does not exist.')
            return ret
        ret['log'].append('Unknown error: ' + str(error_msg))
        return ret

    for e in events:
        eventline = 'StackEvent {ResourceType} {LogicalResourceId} {ResourceStatus}'.format(
            **e)
        ret['events'].append(eventline)

        if e['ResourceStatus'].endswith('FAILED'):
            failline = '{ResourceType} {LogicalResourceId} {ResourceStatus}: {ResourceStatusReason}'.format(
                **e)
            ret['log'].append(failline)

    return ret
Beispiel #2
0
def update_termination_protection(module, cfn, stack_name,
                                  desired_termination_protection_state):
    '''updates termination protection of a stack'''
    if not boto_supports_termination_protection(cfn):
        module.fail_json(
            msg="termination_protection parameter requires botocore >= 1.7.18")
    stack = get_stack_facts(cfn, stack_name)
    if stack:
        if stack[
                'EnableTerminationProtection'] is not desired_termination_protection_state:
            try:
                cfn.update_termination_protection(
                    EnableTerminationProtection=
                    desired_termination_protection_state,
                    StackName=stack_name)
            except botocore.exceptions.ClientError as e:
                module.fail_json(msg=boto_exception(e),
                                 exception=traceback.format_exc())
Beispiel #3
0
def get_stack_facts(cfn, stack_name):
    try:
        stack_response = cfn.describe_stacks(StackName=stack_name)
        stack_info = stack_response['Stacks'][0]
    except (botocore.exceptions.ValidationError,
            botocore.exceptions.ClientError) as err:
        error_msg = boto_exception(err)
        if 'does not exist' in error_msg:
            # missing stack, don't bail.
            return None

        # other error, bail.
        raise err

    if stack_response and stack_response.get('Stacks', None):
        stacks = stack_response['Stacks']
        if len(stacks):
            stack_info = stacks[0]

    return stack_info
Beispiel #4
0
def create_stack(module, stack_params, cfn, events_limit):
    if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
        module.fail_json(
            msg=
            "Either 'template', 'template_body' or 'template_url' is required when the stack does not exist."
        )

    # 'DisableRollback', 'TimeoutInMinutes', 'EnableTerminationProtection' and
    # 'OnFailure' only apply on creation, not update.
    if module.params.get('on_create_failure') is not None:
        stack_params['OnFailure'] = module.params['on_create_failure']
    else:
        stack_params['DisableRollback'] = module.params['disable_rollback']

    if module.params.get('create_timeout') is not None:
        stack_params['TimeoutInMinutes'] = module.params['create_timeout']
    if module.params.get('termination_protection') is not None:
        if boto_supports_termination_protection(cfn):
            stack_params['EnableTerminationProtection'] = bool(
                module.params.get('termination_protection'))
        else:
            module.fail_json(
                msg=
                "termination_protection parameter requires botocore >= 1.7.18")

    try:
        response = cfn.create_stack(**stack_params)
        # Use stack ID to follow stack state in case of on_create_failure = DELETE
        result = stack_operation(cfn, response['StackId'], 'CREATE',
                                 events_limit,
                                 stack_params.get('ClientRequestToken', None))
    except Exception as err:
        error_msg = boto_exception(err)
        module.fail_json(msg="Failed to create stack {0}: {1}.".format(
            stack_params.get('StackName'), error_msg),
                         exception=traceback.format_exc())
    if not result:
        module.fail_json(msg="empty result")
    return result
Beispiel #5
0
def update_stack(module, stack_params, cfn, events_limit):
    if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
        stack_params['UsePreviousTemplate'] = True

    # if the state is present and the stack already exists, we try to update it.
    # AWS will tell us if the stack template and parameters are the same and
    # don't need to be updated.
    try:
        cfn.update_stack(**stack_params)
        result = stack_operation(cfn, stack_params['StackName'], 'UPDATE',
                                 events_limit,
                                 stack_params.get('ClientRequestToken', None))
    except Exception as err:
        error_msg = boto_exception(err)
        if 'No updates are to be performed.' in error_msg:
            result = dict(changed=False, output='Stack is already up-to-date.')
        else:
            module.fail_json(msg="Failed to update stack {0}: {1}".format(
                stack_params.get('StackName'), error_msg),
                             exception=traceback.format_exc())
    if not result:
        module.fail_json(msg="empty result")
    return result
Beispiel #6
0
def check_mode_changeset(module, stack_params, cfn):
    """Create a change set, describe it and delete it before returning check mode outputs."""
    stack_params['ChangeSetName'] = build_changeset_name(stack_params)
    # changesets don't accept ClientRequestToken parameters
    stack_params.pop('ClientRequestToken', None)

    try:
        change_set = cfn.create_change_set(**stack_params)
        for i in range(60):  # total time 5 min
            description = cfn.describe_change_set(
                ChangeSetName=change_set['Id'])
            if description['Status'] in ('CREATE_COMPLETE', 'FAILED'):
                break
            time.sleep(5)
        else:
            # if the changeset doesn't finish in 5 mins, this `else` will trigger and fail
            module.fail_json(msg="Failed to create change set %s" %
                             stack_params['ChangeSetName'])

        cfn.delete_change_set(ChangeSetName=change_set['Id'])

        reason = description.get('StatusReason')

        if description[
                'Status'] == 'FAILED' and "didn't contain changes" in description[
                    'StatusReason']:
            return {
                'changed': False,
                'msg': reason,
                'meta': description['StatusReason']
            }
        return {'changed': True, 'msg': reason, 'meta': description['Changes']}

    except (botocore.exceptions.ValidationError,
            botocore.exceptions.ClientError) as err:
        error_msg = boto_exception(err)
        module.fail_json(msg=error_msg, exception=traceback.format_exc())
def run(ecr, params):
    # type: (EcsEcr, dict, int) -> Tuple[bool, dict]
    result = {}
    try:
        name = params['name']
        state = params['state']
        policy_text = params['policy']
        purge_policy = params['purge_policy']
        registry_id = params['registry_id']
        force_set_policy = params['force_set_policy']
        image_tag_mutability = params['image_tag_mutability'].upper()
        lifecycle_policy_text = params['lifecycle_policy']
        purge_lifecycle_policy = params['purge_lifecycle_policy']

        # Parse policies, if they are given
        try:
            policy = policy_text and json.loads(policy_text)
        except ValueError:
            result['policy'] = policy_text
            result['msg'] = 'Could not parse policy'
            return False, result

        try:
            lifecycle_policy = \
                lifecycle_policy_text and json.loads(lifecycle_policy_text)
        except ValueError:
            result['lifecycle_policy'] = lifecycle_policy_text
            result['msg'] = 'Could not parse lifecycle_policy'
            return False, result

        result['state'] = state
        result['created'] = False

        repo = ecr.get_repository(registry_id, name)

        if state == 'present':
            result['created'] = False

            if not repo:
                repo = ecr.create_repository(registry_id, name,
                                             image_tag_mutability)
                result['changed'] = True
                result['created'] = True
            else:
                repo = ecr.put_image_tag_mutability(registry_id, name,
                                                    image_tag_mutability)
            result['repository'] = repo

            if purge_lifecycle_policy:
                original_lifecycle_policy = \
                    ecr.get_lifecycle_policy(registry_id, name)

                result['lifecycle_policy'] = None

                if original_lifecycle_policy:
                    ecr.purge_lifecycle_policy(registry_id, name)
                    result['changed'] = True

            elif lifecycle_policy_text is not None:
                try:
                    lifecycle_policy = sort_json_policy_dict(lifecycle_policy)
                    result['lifecycle_policy'] = lifecycle_policy

                    original_lifecycle_policy = ecr.get_lifecycle_policy(
                        registry_id, name)

                    if original_lifecycle_policy:
                        original_lifecycle_policy = sort_json_policy_dict(
                            original_lifecycle_policy)

                    if original_lifecycle_policy != lifecycle_policy:
                        ecr.put_lifecycle_policy(registry_id, name,
                                                 lifecycle_policy_text)
                        result['changed'] = True
                except Exception:
                    # Some failure w/ the policy. It's helpful to know what the
                    # policy is.
                    result['lifecycle_policy'] = lifecycle_policy_text
                    raise

            if purge_policy:
                original_policy = ecr.get_repository_policy(registry_id, name)

                result['policy'] = None

                if original_policy:
                    ecr.delete_repository_policy(registry_id, name)
                    result['changed'] = True

            elif policy_text is not None:
                try:
                    # Sort any lists containing only string types
                    policy = sort_lists_of_strings(policy)

                    result['policy'] = policy

                    original_policy = ecr.get_repository_policy(
                        registry_id, name)
                    if original_policy:
                        original_policy = sort_lists_of_strings(
                            original_policy)

                    if compare_policies(original_policy, policy):
                        ecr.set_repository_policy(registry_id, name,
                                                  policy_text,
                                                  force_set_policy)
                        result['changed'] = True
                except Exception:
                    # Some failure w/ the policy. It's helpful to know what the
                    # policy is.
                    result['policy'] = policy_text
                    raise

        elif state == 'absent':
            result['name'] = name
            if repo:
                ecr.delete_repository(registry_id, name)
                result['changed'] = True

    except Exception as err:
        msg = str(err)
        if isinstance(err, ClientError):
            msg = boto_exception(err)
        result['msg'] = msg
        result['exception'] = traceback.format_exc()
        return False, result

    if ecr.skipped:
        result['skipped'] = True

    if ecr.changed:
        result['changed'] = True

    return True, result
Beispiel #8
0
def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(
        dict(
            mode=dict(choices=['push'], default='push'),
            file_change_strategy=dict(
                choices=['force', 'date_size', 'checksum'],
                default='date_size'),
            bucket=dict(required=True),
            key_prefix=dict(required=False, default=''),
            file_root=dict(required=True, type='path'),
            permission=dict(required=False,
                            choices=[
                                'private', 'public-read', 'public-read-write',
                                'authenticated-read', 'aws-exec-read',
                                'bucket-owner-read',
                                'bucket-owner-full-control'
                            ]),
            retries=dict(required=False, removed_in_version='2.14'),
            mime_map=dict(required=False, type='dict'),
            exclude=dict(required=False, default=".*"),
            include=dict(required=False, default="*"),
            cache_control=dict(required=False, default=''),
            delete=dict(required=False, type='bool', default=False),
            # future options: encoding, metadata, storage_class, retries
        ))

    module = AnsibleModule(argument_spec=argument_spec, )

    if not HAS_DATEUTIL:
        module.fail_json(msg='dateutil required for this module')

    if not HAS_BOTO3:
        module.fail_json(msg='boto3 required for this module')

    result = {}
    mode = module.params['mode']

    region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module,
                                                                  boto3=True)
    if not region:
        module.fail_json(msg="Region must be specified")
    s3 = boto3_conn(module,
                    conn_type='client',
                    resource='s3',
                    region=region,
                    endpoint=ec2_url,
                    **aws_connect_kwargs)

    if mode == 'push':
        try:
            result['filelist_initial'] = gather_files(
                module.params['file_root'],
                exclude=module.params['exclude'],
                include=module.params['include'])
            result['filelist_typed'] = determine_mimetypes(
                result['filelist_initial'], module.params.get('mime_map'))
            result['filelist_s3'] = calculate_s3_path(
                result['filelist_typed'], module.params['key_prefix'])
            result['filelist_local_etag'] = calculate_local_etag(
                result['filelist_s3'])
            result['filelist_actionable'] = filter_list(
                s3, module.params['bucket'], result['filelist_local_etag'],
                module.params['file_change_strategy'])
            result['uploads'] = upload_files(s3, module.params['bucket'],
                                             result['filelist_actionable'],
                                             module.params)

            if module.params['delete']:
                result['removed'] = remove_files(s3,
                                                 result['filelist_local_etag'],
                                                 module.params)

            # mark changed if we actually upload something.
            if result.get('uploads') or result.get('removed'):
                result['changed'] = True
            # result.update(filelist=actionable_filelist)
        except botocore.exceptions.ClientError as err:
            error_msg = boto_exception(err)
            module.fail_json(msg=error_msg,
                             exception=traceback.format_exc(),
                             **camel_dict_to_snake_dict(err.response))

    module.exit_json(**result)
Beispiel #9
0
def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(
        dict(stack_name=dict(required=True),
             template_parameters=dict(required=False, type='dict', default={}),
             state=dict(default='present', choices=['present', 'absent']),
             template=dict(default=None, required=False, type='path'),
             notification_arns=dict(default=None, required=False),
             stack_policy=dict(default=None, required=False),
             disable_rollback=dict(default=False, type='bool'),
             on_create_failure=dict(
                 default=None,
                 required=False,
                 choices=['DO_NOTHING', 'ROLLBACK', 'DELETE']),
             create_timeout=dict(default=None, type='int'),
             template_url=dict(default=None, required=False),
             template_body=dict(default=None, required=False),
             template_format=dict(removed_in_version='2.14'),
             create_changeset=dict(default=False, type='bool'),
             changeset_name=dict(default=None, required=False),
             role_arn=dict(default=None, required=False),
             tags=dict(default=None, type='dict'),
             termination_protection=dict(default=None, type='bool'),
             events_limit=dict(default=200, type='int'),
             backoff_retries=dict(type='int', default=10, required=False),
             backoff_delay=dict(type='int', default=3, required=False),
             backoff_max_delay=dict(type='int', default=30, required=False),
             capabilities=dict(
                 type='list',
                 default=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'])))

    module = AnsibleModule(
        argument_spec=argument_spec,
        mutually_exclusive=[['template_url', 'template', 'template_body'],
                            ['disable_rollback', 'on_create_failure']],
        supports_check_mode=True)
    if not HAS_BOTO3:
        module.fail_json(msg='boto3 and botocore are required for this module')

    invalid_capabilities = []
    user_capabilities = module.params.get('capabilities')
    for user_cap in user_capabilities:
        if user_cap not in [
                'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM',
                'CAPABILITY_AUTO_EXPAND'
        ]:
            invalid_capabilities.append(user_cap)

    if invalid_capabilities:
        module.fail_json(msg="Specified capabilities are invalid : %r,"
                         " please check documentation for valid capabilities" %
                         invalid_capabilities)

    # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around.
    stack_params = {
        'Capabilities': user_capabilities,
        'ClientRequestToken': to_native(uuid.uuid4()),
    }
    state = module.params['state']
    stack_params['StackName'] = module.params['stack_name']

    if module.params['template'] is not None:
        with open(module.params['template'], 'r') as template_fh:
            stack_params['TemplateBody'] = template_fh.read()
    elif module.params['template_body'] is not None:
        stack_params['TemplateBody'] = module.params['template_body']
    elif module.params['template_url'] is not None:
        stack_params['TemplateURL'] = module.params['template_url']

    if module.params.get('notification_arns'):
        stack_params['NotificationARNs'] = module.params[
            'notification_arns'].split(',')
    else:
        stack_params['NotificationARNs'] = []

    # can't check the policy when verifying.
    if module.params[
            'stack_policy'] is not None and not module.check_mode and not module.params[
                'create_changeset']:
        with open(module.params['stack_policy'], 'r') as stack_policy_fh:
            stack_params['StackPolicyBody'] = stack_policy_fh.read()

    template_parameters = module.params['template_parameters']

    stack_params['Parameters'] = []
    for k, v in template_parameters.items():
        if isinstance(v, dict):
            # set parameter based on a dict to allow additional CFN Parameter Attributes
            param = dict(ParameterKey=k)

            if 'value' in v:
                param['ParameterValue'] = str(v['value'])

            if 'use_previous_value' in v and bool(v['use_previous_value']):
                param['UsePreviousValue'] = True
                param.pop('ParameterValue', None)

            stack_params['Parameters'].append(param)
        else:
            # allow default k/v configuration to set a template parameter
            stack_params['Parameters'].append({
                'ParameterKey': k,
                'ParameterValue': str(v)
            })

    if isinstance(module.params.get('tags'), dict):
        stack_params['Tags'] = ansible_dict_to_boto3_tag_list(
            module.params['tags'])

    if module.params.get('role_arn'):
        stack_params['RoleARN'] = module.params['role_arn']

    result = {}

    try:
        region, ec2_url, aws_connect_kwargs = get_aws_connection_info(
            module, boto3=True)
        cfn = boto3_conn(module,
                         conn_type='client',
                         resource='cloudformation',
                         region=region,
                         endpoint=ec2_url,
                         **aws_connect_kwargs)
    except botocore.exceptions.NoCredentialsError as e:
        module.fail_json(msg=boto_exception(e))

    # Wrap the cloudformation client methods that this module uses with
    # automatic backoff / retry for throttling error codes
    backoff_wrapper = AWSRetry.jittered_backoff(
        retries=module.params.get('backoff_retries'),
        delay=module.params.get('backoff_delay'),
        max_delay=module.params.get('backoff_max_delay'))
    cfn.describe_stack_events = backoff_wrapper(cfn.describe_stack_events)
    cfn.create_stack = backoff_wrapper(cfn.create_stack)
    cfn.list_change_sets = backoff_wrapper(cfn.list_change_sets)
    cfn.create_change_set = backoff_wrapper(cfn.create_change_set)
    cfn.update_stack = backoff_wrapper(cfn.update_stack)
    cfn.describe_stacks = backoff_wrapper(cfn.describe_stacks)
    cfn.list_stack_resources = backoff_wrapper(cfn.list_stack_resources)
    cfn.delete_stack = backoff_wrapper(cfn.delete_stack)
    if boto_supports_termination_protection(cfn):
        cfn.update_termination_protection = backoff_wrapper(
            cfn.update_termination_protection)

    stack_info = get_stack_facts(cfn, stack_params['StackName'])

    if module.check_mode:
        if state == 'absent' and stack_info:
            module.exit_json(changed=True,
                             msg='Stack would be deleted',
                             meta=[])
        elif state == 'absent' and not stack_info:
            module.exit_json(changed=False,
                             msg='Stack doesn\'t exist',
                             meta=[])
        elif state == 'present' and not stack_info:
            module.exit_json(changed=True,
                             msg='New stack would be created',
                             meta=[])
        else:
            module.exit_json(**check_mode_changeset(module, stack_params, cfn))

    if state == 'present':
        if not stack_info:
            result = create_stack(module, stack_params, cfn,
                                  module.params.get('events_limit'))
        elif module.params.get('create_changeset'):
            result = create_changeset(module, stack_params, cfn,
                                      module.params.get('events_limit'))
        else:
            if module.params.get('termination_protection') is not None:
                update_termination_protection(
                    module, cfn, stack_params['StackName'],
                    bool(module.params.get('termination_protection')))
            result = update_stack(module, stack_params, cfn,
                                  module.params.get('events_limit'))

        # format the stack output

        stack = get_stack_facts(cfn, stack_params['StackName'])
        if stack is not None:
            if result.get('stack_outputs') is None:
                # always define stack_outputs, but it may be empty
                result['stack_outputs'] = {}
            for output in stack.get('Outputs', []):
                result['stack_outputs'][
                    output['OutputKey']] = output['OutputValue']
            stack_resources = []
            reslist = cfn.list_stack_resources(
                StackName=stack_params['StackName'])
            for res in reslist.get('StackResourceSummaries', []):
                stack_resources.append({
                    "logical_resource_id":
                    res['LogicalResourceId'],
                    "physical_resource_id":
                    res.get('PhysicalResourceId', ''),
                    "resource_type":
                    res['ResourceType'],
                    "last_updated_time":
                    res['LastUpdatedTimestamp'],
                    "status":
                    res['ResourceStatus'],
                    "status_reason":
                    res.get('ResourceStatusReason')  # can be blank, apparently
                })
            result['stack_resources'] = stack_resources

    elif state == 'absent':
        # absent state is different because of the way delete_stack works.
        # problem is it it doesn't give an error if stack isn't found
        # so must describe the stack first

        try:
            stack = get_stack_facts(cfn, stack_params['StackName'])
            if not stack:
                result = {'changed': False, 'output': 'Stack not found.'}
            else:
                if stack_params.get('RoleARN') is None:
                    cfn.delete_stack(StackName=stack_params['StackName'])
                else:
                    cfn.delete_stack(StackName=stack_params['StackName'],
                                     RoleARN=stack_params['RoleARN'])
                result = stack_operation(
                    cfn, stack_params['StackName'], 'DELETE',
                    module.params.get('events_limit'),
                    stack_params.get('ClientRequestToken', None))
        except Exception as err:
            module.fail_json(msg=boto_exception(err),
                             exception=traceback.format_exc())

    module.exit_json(**result)
Beispiel #10
0
def create_changeset(module, stack_params, cfn, events_limit):
    if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
        module.fail_json(
            msg="Either 'template' or 'template_url' is required.")
    if module.params['changeset_name'] is not None:
        stack_params['ChangeSetName'] = module.params['changeset_name']

    # changesets don't accept ClientRequestToken parameters
    stack_params.pop('ClientRequestToken', None)

    try:
        changeset_name = build_changeset_name(stack_params)
        stack_params['ChangeSetName'] = changeset_name

        # Determine if this changeset already exists
        pending_changesets = list_changesets(cfn, stack_params['StackName'])
        if changeset_name in pending_changesets:
            warning = 'WARNING: %d pending changeset(s) exist(s) for this stack!' % len(
                pending_changesets)
            result = dict(changed=False,
                          output='ChangeSet %s already exists.' %
                          changeset_name,
                          warnings=[warning])
        else:
            cs = cfn.create_change_set(**stack_params)
            # Make sure we don't enter an infinite loop
            time_end = time.time() + 600
            while time.time() < time_end:
                try:
                    newcs = cfn.describe_change_set(ChangeSetName=cs['Id'])
                except botocore.exceptions.BotoCoreError as err:
                    error_msg = boto_exception(err)
                    module.fail_json(msg=error_msg)
                if newcs['Status'] == 'CREATE_PENDING' or newcs[
                        'Status'] == 'CREATE_IN_PROGRESS':
                    time.sleep(1)
                elif newcs[
                        'Status'] == 'FAILED' and "The submitted information didn't contain changes" in newcs[
                            'StatusReason']:
                    cfn.delete_change_set(ChangeSetName=cs['Id'])
                    result = dict(
                        changed=False,
                        output=
                        'The created Change Set did not contain any changes to this stack and was deleted.'
                    )
                    # a failed change set does not trigger any stack events so we just want to
                    # skip any further processing of result and just return it directly
                    return result
                else:
                    break
                # Lets not hog the cpu/spam the AWS API
                time.sleep(1)
            result = stack_operation(cfn, stack_params['StackName'],
                                     'CREATE_CHANGESET', events_limit)
            result['change_set_id'] = cs['Id']
            result['warnings'] = [
                'Created changeset named %s for stack %s' %
                (changeset_name, stack_params['StackName']),
                'You can execute it using: aws cloudformation execute-change-set --change-set-name %s'
                % cs['Id'],
                'NOTE that dependencies on this stack might fail due to pending changes!'
            ]
    except Exception as err:
        error_msg = boto_exception(err)
        if 'No updates are to be performed.' in error_msg:
            result = dict(changed=False, output='Stack is already up-to-date.')
        else:
            module.fail_json(
                msg="Failed to create change set: {0}".format(error_msg),
                exception=traceback.format_exc())

    if not result:
        module.fail_json(msg="empty result")
    return result