diff --git a/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml b/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml index ba80aa7a61f..cf700bc0ca0 100644 --- a/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml +++ b/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml @@ -24,3 +24,12 @@ - assert: that: - "result.data == {'results': [{'i_a_m_user': 'UserName', 'tags': {'do_convert': 'DoNotConvert'}}], 'tags': {'DoNotConvert': 'DoNotConvert'}}" + +- name: Test converting dict keys in lists within lists + convert_snake_case: + data: {'Results': [{'Changes': [{'DoConvert': 'DoNotConvert', 'Details': ['DoNotConvert']}]}]} + register: result + +- assert: + that: + - "result.data == {'results': [{'changes': [{'do_convert': 'DoNotConvert', 'details': ['DoNotConvert']}]}]}" diff --git a/test/integration/targets/incidental_cloudformation/aliases b/test/integration/targets/incidental_cloudformation/aliases deleted file mode 100644 index 29f60feb446..00000000000 --- a/test/integration/targets/incidental_cloudformation/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/incidental diff --git a/test/integration/targets/incidental_cloudformation/defaults/main.yml b/test/integration/targets/incidental_cloudformation/defaults/main.yml deleted file mode 100644 index aaf0ca7e616..00000000000 --- a/test/integration/targets/incidental_cloudformation/defaults/main.yml +++ /dev/null @@ -1,8 +0,0 @@ -stack_name: "{{ resource_prefix }}" - -vpc_name: '{{ resource_prefix }}-vpc' -vpc_seed: '{{ resource_prefix }}' -vpc_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.0.0/16' -subnet_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' - -ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2' diff --git a/test/integration/targets/incidental_cloudformation/files/cf_template.json b/test/integration/targets/incidental_cloudformation/files/cf_template.json deleted file mode 100644 index ff4c5693b0b..00000000000 --- a/test/integration/targets/incidental_cloudformation/files/cf_template.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "AWSTemplateFormatVersion" : "2010-09-09", - - "Description" : "Create an Amazon EC2 instance.", - - "Parameters" : { - "InstanceType" : { - "Description" : "EC2 instance type", - "Type" : "String", - "Default" : "t3.nano", - "AllowedValues" : [ "t3.micro", "t3.nano"] - }, - "ImageId" : { - "Type" : "String" - }, - "SubnetId" : { - "Type" : "String" - } - }, - - "Resources" : { - "EC2Instance" : { - "Type" : "AWS::EC2::Instance", - "Properties" : { - "InstanceType" : { "Ref" : "InstanceType" }, - "ImageId" : { "Ref" : "ImageId" }, - "SubnetId": { "Ref" : "SubnetId" } - } - } - }, - - "Outputs" : { - "InstanceId" : { - "Value" : { "Ref" : "EC2Instance" } - } - } -} diff --git a/test/integration/targets/incidental_cloudformation/tasks/main.yml b/test/integration/targets/incidental_cloudformation/tasks/main.yml deleted file mode 100644 index 10924bcd531..00000000000 --- a/test/integration/targets/incidental_cloudformation/tasks/main.yml +++ /dev/null @@ -1,476 +0,0 @@ ---- -- name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key | default(omit) }}" - aws_secret_key: "{{ aws_secret_key | default(omit) }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region | default(omit) }}" - no_log: yes - -- module_defaults: - cloudformation: - <<: *aws_connection_info - cloudformation_info: - <<: *aws_connection_info - - block: - - # ==== Env setup ========================================================== - - name: list available AZs - aws_az_info: - <<: *aws_connection_info - register: region_azs - - - name: pick an AZ for testing - set_fact: - availability_zone: "{{ region_azs.availability_zones[0].zone_name }}" - - - name: Create a test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - tags: - Name: Cloudformation testing - <<: *aws_connection_info - register: testing_vpc - - - name: Create a test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - az: "{{ availability_zone }}" - <<: *aws_connection_info - register: testing_subnet - - - name: Find AMI to use - ec2_ami_info: - owners: 'amazon' - filters: - name: '{{ ec2_ami_name }}' - <<: *aws_connection_info - register: ec2_amis - - - name: Set fact with latest AMI - vars: - latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' - set_fact: - ec2_ami_image: '{{ latest_ami.image_id }}' - - # ==== Cloudformation tests =============================================== - - # 1. Basic stack creation (check mode, actual run and idempotency) - # 2. Tags - # 3. cloudformation_info tests (basic + all_facts) - # 4. termination_protection - # 5. create_changeset + changeset_name - - # There is still scope to add tests for - - # 1. capabilities - # 2. stack_policy - # 3. on_create_failure (covered in unit tests) - # 4. Passing in a role - # 5. nested stacks? - - - - name: create a cloudformation stack (check mode) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - check_mode: yes - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'msg' in cf_stack and 'New stack would be created' in cf_stack.msg" - - - name: create a cloudformation stack - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'events' in cf_stack" - - "'output' in cf_stack and 'Stack CREATE complete' in cf_stack.output" - - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - - "'stack_resources' in cf_stack" - - - name: create a cloudformation stack (check mode) (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - check_mode: yes - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - - name: create a cloudformation stack (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'output' in cf_stack and 'Stack is already up-to-date.' in cf_stack.output" - - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - - "'stack_resources' in cf_stack" - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "'cloudformation' in stack_info" - - "stack_info.cloudformation | length == 1" - - "stack_name in stack_info.cloudformation" - - "'stack_description' in stack_info.cloudformation[stack_name]" - - "'stack_outputs' in stack_info.cloudformation[stack_name]" - - "'stack_parameters' in stack_info.cloudformation[stack_name]" - - "'stack_tags' in stack_info.cloudformation[stack_name]" - - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "'cloudformation' in stack_info" - - "stack_info.cloudformation | length == 1" - - "stack_name in stack_info.cloudformation" - - "'stack_description' in stack_info.cloudformation[stack_name]" - - "'stack_outputs' in stack_info.cloudformation[stack_name]" - - "'stack_parameters' in stack_info.cloudformation[stack_name]" - - "'stack_tags' in stack_info.cloudformation[stack_name]" - - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - - - name: get stack details (all_facts) - cloudformation_info: - stack_name: "{{ stack_name }}" - all_facts: yes - register: stack_info - - - name: assert stack info - assert: - that: - - "'stack_events' in stack_info.cloudformation[stack_name]" - - "'stack_policy' in stack_info.cloudformation[stack_name]" - - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - - "'stack_resources' in stack_info.cloudformation[stack_name]" - - "'stack_template' in stack_info.cloudformation[stack_name]" - - - name: get stack details (all_facts) (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - all_facts: yes - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "'stack_events' in stack_info.cloudformation[stack_name]" - - "'stack_policy' in stack_info.cloudformation[stack_name]" - - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - - "'stack_resources' in stack_info.cloudformation[stack_name]" - - "'stack_template' in stack_info.cloudformation[stack_name]" - - # ==== Cloudformation tests (create changeset) ============================ - - # try to create a changeset by changing instance type - - name: create a changeset - cloudformation: - stack_name: "{{ stack_name }}" - create_changeset: yes - changeset_name: "test-changeset" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.micro" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: create_changeset_result - - - name: assert changeset created - assert: - that: - - "create_changeset_result.changed" - - "'change_set_id' in create_changeset_result" - - "'Stack CREATE_CHANGESET complete' in create_changeset_result.output" - - - name: get stack details with changesets - cloudformation_info: - stack_name: "{{ stack_name }}" - stack_change_sets: True - register: stack_info - - - name: assert changesets in info - assert: - that: - - "'stack_change_sets' in stack_info.cloudformation[stack_name]" - - - name: get stack details with changesets (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - stack_change_sets: True - register: stack_info - check_mode: yes - - - name: assert changesets in info - assert: - that: - - "'stack_change_sets' in stack_info.cloudformation[stack_name]" - - # try to create an empty changeset by passing in unchanged template - - name: create a changeset - cloudformation: - stack_name: "{{ stack_name }}" - create_changeset: yes - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: create_changeset_result - - - name: assert changeset created - assert: - that: - - "not create_changeset_result.changed" - - "'The created Change Set did not contain any changes to this stack and was deleted.' in create_changeset_result.output" - - # ==== Cloudformation tests (termination_protection) ====================== - - - name: set termination protection to true - cloudformation: - stack_name: "{{ stack_name }}" - termination_protection: yes - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - -# This fails - #65592 -# - name: check task return attributes -# assert: -# that: -# - cf_stack.changed - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: set termination protection to false - cloudformation: - stack_name: "{{ stack_name }}" - termination_protection: no - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - -# This fails - #65592 -# - name: check task return attributes -# assert: -# that: -# - cf_stack.changed - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - # ==== Cloudformation tests (delete stack tests) ========================== - - - name: delete cloudformation stack (check mode) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - check_mode: yes - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'msg' in cf_stack and 'Stack would be deleted' in cf_stack.msg" - - - name: delete cloudformation stack - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'output' in cf_stack and 'Stack Deleted' in cf_stack.output" - - - name: delete cloudformation stack (check mode) (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - check_mode: yes - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'msg' in cf_stack" - - >- - "Stack doesn't exist" in cf_stack.msg - - - name: delete cloudformation stack (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'output' in cf_stack and 'Stack not found.' in cf_stack.output" - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation" - - # ==== Cleanup ============================================================ - - always: - - - name: delete stack - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - ignore_errors: yes - - - name: Delete test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: Delete test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - state: absent - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/support/integration/plugins/modules/aws_az_info.py b/test/support/integration/plugins/modules/aws_az_info.py deleted file mode 100644 index c1efed6f0b9..00000000000 --- a/test/support/integration/plugins/modules/aws_az_info.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = ''' -module: aws_az_info -short_description: Gather information about availability zones in AWS. -description: - - Gather information about availability zones in AWS. - - This module was called C(aws_az_facts) before Ansible 2.9. The usage did not change. -version_added: '2.5' -author: 'Henrique Rodrigues (@Sodki)' -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeAvailabilityZones.html) for - possible filters. Filter names and values are case sensitive. You can also use underscores - instead of dashes (-) in the filter keys, which will take precedence in case of conflict. - required: false - default: {} - type: dict -extends_documentation_fragment: - - aws - - ec2 -requirements: [botocore, boto3] -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all availability zones -- aws_az_info: - -# Gather information about a single availability zone -- aws_az_info: - filters: - zone-name: eu-west-1a -''' - -RETURN = ''' -availability_zones: - returned: on success - description: > - Availability zones that match the provided filters. Each element consists of a dict with all the information - related to that available zone. - type: list - sample: "[ - { - 'messages': [], - 'region_name': 'us-west-1', - 'state': 'available', - 'zone_name': 'us-west-1b' - }, - { - 'messages': [], - 'region_name': 'us-west-1', - 'state': 'available', - 'zone_name': 'us-west-1c' - } - ]" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry, ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - - -def main(): - argument_spec = dict( - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - if module._name == 'aws_az_facts': - module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'", - version='2.14', collection_name='ansible.builtin') - - connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - - # Replace filter key underscores with dashes, for compatibility - sanitized_filters = dict((k.replace('_', '-'), v) for k, v in module.params.get('filters').items()) - - try: - availability_zones = connection.describe_availability_zones( - Filters=ansible_dict_to_boto3_filter_list(sanitized_filters) - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to describe availability zones.") - - # Turn the boto3 result into ansible_friendly_snaked_names - snaked_availability_zones = [camel_dict_to_snake_dict(az) for az in availability_zones['AvailabilityZones']] - - module.exit_json(availability_zones=snaked_availability_zones) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/cloudformation.py b/test/support/integration/plugins/modules/cloudformation.py deleted file mode 100644 index cd03146501e..00000000000 --- a/test/support/integration/plugins/modules/cloudformation.py +++ /dev/null @@ -1,837 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: cloudformation -short_description: Create or delete an AWS CloudFormation stack -description: - - Launches or updates an AWS CloudFormation stack and waits for it complete. -notes: - - CloudFormation features change often, and this module tries to keep up. That means your botocore version should be fresh. - The version listed in the requirements is the oldest version that works with the module as a whole. - Some features may require recent versions, and we do not pinpoint a minimum version for each feature. - Instead of relying on the minimum version, keep botocore up to date. AWS is always releasing features and fixing bugs. -version_added: "1.1" -options: - stack_name: - description: - - Name of the CloudFormation stack. - required: true - type: str - disable_rollback: - description: - - If a stacks fails to form, rollback will remove the stack. - default: false - type: bool - on_create_failure: - description: - - Action to take upon failure of stack creation. Incompatible with the I(disable_rollback) option. - choices: - - DO_NOTHING - - ROLLBACK - - DELETE - version_added: "2.8" - type: str - create_timeout: - description: - - The amount of time (in minutes) that can pass before the stack status becomes CREATE_FAILED - version_added: "2.6" - type: int - template_parameters: - description: - - A list of hashes of all the template variables for the stack. The value can be a string or a dict. - - Dict can be used to set additional template parameter attributes like UsePreviousValue (see example). - default: {} - type: dict - state: - description: - - If I(state=present), stack will be created. - - If I(state=present) and if stack exists and template has changed, it will be updated. - - If I(state=absent), stack will be removed. - default: present - choices: [ present, absent ] - type: str - template: - description: - - The local path of the CloudFormation template. - - This must be the full path to the file, relative to the working directory. If using roles this may look - like C(roles/cloudformation/files/cloudformation-example.json). - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), - I(template_body) nor I(template_url) are specified, the previous template will be reused. - type: path - notification_arns: - description: - - A comma separated list of Simple Notification Service (SNS) topic ARNs to publish stack related events. - version_added: "2.0" - type: str - stack_policy: - description: - - The path of the CloudFormation stack policy. A policy cannot be removed once placed, but it can be modified. - for instance, allow all updates U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051) - version_added: "1.9" - type: str - tags: - description: - - Dictionary of tags to associate with stack and its resources during stack creation. - - Can be updated later, updating tags removes previous entries. - version_added: "1.4" - type: dict - template_url: - description: - - Location of file containing the template body. The URL must point to a template (max size 307,200 bytes) located in an - S3 bucket in the same region as the stack. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) are specified, - the previous template will be reused. - version_added: "2.0" - type: str - create_changeset: - description: - - "If stack already exists create a changeset instead of directly applying changes. See the AWS Change Sets docs - U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)." - - "WARNING: if the stack does not exist, it will be created without changeset. If I(state=absent), the stack will be - deleted immediately with no changeset." - type: bool - default: false - version_added: "2.4" - changeset_name: - description: - - Name given to the changeset when creating a changeset. - - Only used when I(create_changeset=true). - - By default a name prefixed with Ansible-STACKNAME is generated based on input parameters. - See the AWS Change Sets docs for more information - U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html) - version_added: "2.4" - type: str - template_format: - description: - - This parameter is ignored since Ansible 2.3 and will be removed in Ansible 2.14. - - Templates are now passed raw to CloudFormation regardless of format. - version_added: "2.0" - type: str - role_arn: - description: - - The role that AWS CloudFormation assumes to create the stack. See the AWS CloudFormation Service Role - docs U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) - version_added: "2.3" - type: str - termination_protection: - description: - - Enable or disable termination protection on the stack. Only works with botocore >= 1.7.18. - type: bool - version_added: "2.5" - template_body: - description: - - Template body. Use this to pass in the actual body of the CloudFormation template. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) - are specified, the previous template will be reused. - version_added: "2.5" - type: str - events_limit: - description: - - Maximum number of CloudFormation events to fetch from a stack when creating or updating it. - default: 200 - version_added: "2.7" - type: int - backoff_delay: - description: - - Number of seconds to wait for the next retry. - default: 3 - version_added: "2.8" - type: int - required: False - backoff_max_delay: - description: - - Maximum amount of time to wait between retries. - default: 30 - version_added: "2.8" - type: int - required: False - backoff_retries: - description: - - Number of times to retry operation. - - AWS API throttling mechanism fails CloudFormation module so we have to retry a couple of times. - default: 10 - version_added: "2.8" - type: int - required: False - capabilities: - description: - - Specify capabilities that stack template contains. - - Valid values are C(CAPABILITY_IAM), C(CAPABILITY_NAMED_IAM) and C(CAPABILITY_AUTO_EXPAND). - type: list - elements: str - version_added: "2.8" - default: [ CAPABILITY_IAM, CAPABILITY_NAMED_IAM ] - -author: "James S. Martin (@jsmartin)" -extends_documentation_fragment: -- aws -- ec2 -requirements: [ boto3, botocore>=1.5.45 ] -''' - -EXAMPLES = ''' -- name: create a cloudformation stack - cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - disable_rollback: true - template: "files/cloudformation-example.json" - template_parameters: - KeyName: "jmartin" - DiskType: "ephemeral" - InstanceType: "m1.small" - ClusterSize: 3 - tags: - Stack: "ansible-cloudformation" - -# Basic role example -- name: create a stack, specify role that cloudformation assumes - cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - disable_rollback: true - template: "roles/cloudformation/files/cloudformation-example.json" - role_arn: 'arn:aws:iam::123456789012:role/cloudformation-iam-role' - -- name: delete a stack - cloudformation: - stack_name: "ansible-cloudformation-old" - state: "absent" - -# Create a stack, pass in template from a URL, disable rollback if stack creation fails, -# pass in some parameters to the template, provide tags for resources created -- name: create a stack, pass in the template via an URL - cloudformation: - stack_name: "ansible-cloudformation" - state: present - region: us-east-1 - disable_rollback: true - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - template_parameters: - KeyName: jmartin - DiskType: ephemeral - InstanceType: m1.small - ClusterSize: 3 - tags: - Stack: ansible-cloudformation - -# Create a stack, passing in template body using lookup of Jinja2 template, disable rollback if stack creation fails, -# pass in some parameters to the template, provide tags for resources created -- name: create a stack, pass in the template body via lookup template - cloudformation: - stack_name: "ansible-cloudformation" - state: present - region: us-east-1 - disable_rollback: true - template_body: "{{ lookup('template', 'cloudformation.j2') }}" - template_parameters: - KeyName: jmartin - DiskType: ephemeral - InstanceType: m1.small - ClusterSize: 3 - tags: - Stack: ansible-cloudformation - -# Pass a template parameter which uses CloudFormation's UsePreviousValue attribute -# When use_previous_value is set to True, the given value will be ignored and -# CloudFormation will use the value from a previously submitted template. -# If use_previous_value is set to False (default) the given value is used. -- cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - template: "files/cloudformation-example.json" - template_parameters: - DBSnapshotIdentifier: - use_previous_value: True - value: arn:aws:rds:es-east-1:000000000000:snapshot:rds:my-db-snapshot - DBName: - use_previous_value: True - tags: - Stack: "ansible-cloudformation" - -# Enable termination protection on a stack. -# If the stack already exists, this will update its termination protection -- name: enable termination protection during stack creation - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - termination_protection: yes - -# Configure TimeoutInMinutes before the stack status becomes CREATE_FAILED -# In this case, if disable_rollback is not set or is set to false, the stack will be rolled back. -- name: enable termination protection during stack creation - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - create_timeout: 5 - -# Configure rollback behaviour on the unsuccessful creation of a stack allowing -# CloudFormation to clean up, or do nothing in the event of an unsuccessful -# deployment -# In this case, if on_create_failure is set to "DELETE", it will clean up the stack if -# it fails to create -- name: create stack which will delete on creation failure - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - on_create_failure: DELETE -''' - -RETURN = ''' -events: - type: list - description: Most recent events in CloudFormation's event log. This may be from a previous run in some cases. - returned: always - sample: ["StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE", "StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"] -log: - description: Debugging logs. Useful when modifying or finding an error. - returned: always - type: list - sample: ["updating stack"] -change_set_id: - description: The ID of the stack change set if one was created - returned: I(state=present) and I(create_changeset=true) - type: str - sample: "arn:aws:cloudformation:us-east-1:012345678901:changeSet/Ansible-StackName-f4496805bd1b2be824d1e315c6884247ede41eb0" -stack_resources: - description: AWS stack resources and their status. List of dictionaries, one dict per resource. - returned: state == present - type: list - sample: [ - { - "last_updated_time": "2016-10-11T19:40:14.979000+00:00", - "logical_resource_id": "CFTestSg", - "physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F", - "resource_type": "AWS::EC2::SecurityGroup", - "status": "UPDATE_COMPLETE", - "status_reason": null - } - ] -stack_outputs: - type: dict - description: A key:value dictionary of all the stack outputs currently defined. If there are no stack outputs, it is an empty dictionary. - returned: state == present - sample: {"MySg": "AnsibleModuleTestYAML-CFTestSg-C8UVS567B6NS"} -''' # NOQA - -import json -import time -import uuid -import traceback -from hashlib import sha1 - -try: - import boto3 - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, AWSRetry, boto3_conn, boto_exception, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes, to_native - - -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 - - -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 - - -def list_changesets(cfn, stack_name): - res = cfn.list_change_sets(StackName=stack_name) - return [cs['ChangeSetName'] for cs in res['Summaries']] - - -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 - - -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 - - -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()) - - -def boto_supports_termination_protection(cfn): - '''termination protection was added in botocore 1.7.18''' - return hasattr(cfn, "update_termination_protection") - - -def stack_operation(cfn, stack_name, operation, events_limit, op_token=None): - '''gets the status of a stack while it is created/updated/deleted''' - existed = [] - while True: - try: - stack = get_stack_facts(cfn, stack_name) - existed.append('yes') - except Exception: - # If the stack previously existed, and now can't be found then it's - # been deleted successfully. - if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - ret.update({'changed': True, 'output': 'Stack Deleted'}) - return ret - else: - return {'changed': True, 'failed': True, 'output': 'Stack Not Found', 'exception': traceback.format_exc()} - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - if not stack: - if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - ret.update({'changed': True, 'output': 'Stack Deleted'}) - return ret - else: - ret.update({'changed': False, 'failed': True, 'output': 'Stack not found.'}) - return ret - # it covers ROLLBACK_COMPLETE and UPDATE_ROLLBACK_COMPLETE - # Possible states: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w1ab2c15c17c21c13 - elif stack['StackStatus'].endswith('ROLLBACK_COMPLETE') and operation != 'CREATE_CHANGESET': - ret.update({'changed': True, 'failed': True, 'output': 'Problem with %s. Rollback complete' % operation}) - return ret - elif stack['StackStatus'] == 'DELETE_COMPLETE' and operation == 'CREATE': - ret.update({'changed': True, 'failed': True, 'output': 'Stack create failed. Delete complete.'}) - return ret - # note the ordering of ROLLBACK_COMPLETE, DELETE_COMPLETE, and COMPLETE, because otherwise COMPLETE will match all cases. - elif stack['StackStatus'].endswith('_COMPLETE'): - ret.update({'changed': True, 'output': 'Stack %s complete' % operation}) - return ret - elif stack['StackStatus'].endswith('_ROLLBACK_FAILED'): - ret.update({'changed': True, 'failed': True, 'output': 'Stack %s rollback failed' % operation}) - return ret - # note the ordering of ROLLBACK_FAILED and FAILED, because otherwise FAILED will match both cases. - elif stack['StackStatus'].endswith('_FAILED'): - ret.update({'changed': True, 'failed': True, 'output': 'Stack %s failed' % operation}) - return ret - else: - # this can loop forever :/ - time.sleep(5) - return {'failed': True, 'output': 'Failed for unknown reasons.'} - - -def build_changeset_name(stack_params): - if 'ChangeSetName' in stack_params: - return stack_params['ChangeSetName'] - - json_params = json.dumps(stack_params, sort_keys=True) - - return 'Ansible-{0}-{1}'.format( - stack_params['StackName'], - sha1(to_bytes(json_params, errors='surrogate_or_strict')).hexdigest() - ) - - -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 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 - - -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) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/cloudformation_info.py b/test/support/integration/plugins/modules/cloudformation_info.py deleted file mode 100644 index ee2e5c178b3..00000000000 --- a/test/support/integration/plugins/modules/cloudformation_info.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudformation_info -short_description: Obtain information about an AWS CloudFormation stack -description: - - Gets information about an AWS CloudFormation stack. - - This module was called C(cloudformation_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(cloudformation_info) module no longer returns C(ansible_facts)! -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 -version_added: "2.2" -author: - - Justin Menga (@jmenga) - - Kevin Coming (@waffie1) -options: - stack_name: - description: - - The name or id of the CloudFormation stack. Gathers information on all stacks by default. - type: str - all_facts: - description: - - Get all stack information for the stack. - type: bool - default: false - stack_events: - description: - - Get stack events for the stack. - type: bool - default: false - stack_template: - description: - - Get stack template body for the stack. - type: bool - default: false - stack_resources: - description: - - Get stack resources for the stack. - type: bool - default: false - stack_policy: - description: - - Get stack policy for the stack. - type: bool - default: false - stack_change_sets: - description: - - Get stack change sets for the stack - type: bool - default: false - version_added: '2.10' -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Get summary information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - register: output - -- debug: - msg: "{{ output['cloudformation']['my-cloudformation-stack'] }}" - -# When the module is called as cloudformation_facts, return values are published -# in ansible_facts['cloudformation'][] and can be used as follows. -# Note that this is deprecated and will stop working in Ansible 2.13. - -- cloudformation_facts: - stack_name: my-cloudformation-stack - -- debug: - msg: "{{ ansible_facts['cloudformation']['my-cloudformation-stack'] }}" - -# Get stack outputs, when you have the stack name available as a fact -- set_fact: - stack_name: my-awesome-stack - -- cloudformation_info: - stack_name: "{{ stack_name }}" - register: my_stack - -- debug: - msg: "{{ my_stack.cloudformation[stack_name].stack_outputs }}" - -# Get all stack information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - all_facts: true - -# Get stack resource and stack policy information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - stack_resources: true - stack_policy: true - -# Fail if the stack doesn't exist -- name: try to get facts about a stack but fail if it doesn't exist - cloudformation_info: - stack_name: nonexistent-stack - all_facts: yes - failed_when: cloudformation['nonexistent-stack'] is undefined -''' - -RETURN = ''' -stack_description: - description: Summary facts about the stack - returned: if the stack exists - type: dict -stack_outputs: - description: Dictionary of stack outputs keyed by the value of each output 'OutputKey' parameter and corresponding value of each - output 'OutputValue' parameter - returned: if the stack exists - type: dict - sample: - ApplicationDatabaseName: dazvlpr01xj55a.ap-southeast-2.rds.amazonaws.com -stack_parameters: - description: Dictionary of stack parameters keyed by the value of each parameter 'ParameterKey' parameter and corresponding value of - each parameter 'ParameterValue' parameter - returned: if the stack exists - type: dict - sample: - DatabaseEngine: mysql - DatabasePassword: "***" -stack_events: - description: All stack events for the stack - returned: only if all_facts or stack_events is true and the stack exists - type: list -stack_policy: - description: Describes the stack policy for the stack - returned: only if all_facts or stack_policy is true and the stack exists - type: dict -stack_template: - description: Describes the stack template for the stack - returned: only if all_facts or stack_template is true and the stack exists - type: dict -stack_resource_list: - description: Describes stack resources for the stack - returned: only if all_facts or stack_resourses is true and the stack exists - type: list -stack_resources: - description: Dictionary of stack resources keyed by the value of each resource 'LogicalResourceId' parameter and corresponding value of each - resource 'PhysicalResourceId' parameter - returned: only if all_facts or stack_resourses is true and the stack exists - type: dict - sample: - AutoScalingGroup: "dev-someapp-AutoscalingGroup-1SKEXXBCAN0S7" - AutoScalingSecurityGroup: "sg-abcd1234" - ApplicationDatabase: "dazvlpr01xj55a" -stack_change_sets: - description: A list of stack change sets. Each item in the list represents the details of a specific changeset - - returned: only if all_facts or stack_change_sets is true and the stack exists - type: list -''' - -import json -import traceback - -from functools import partial -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, AWSRetry, boto3_tag_list_to_ansible_dict) - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - - -class CloudFormationServiceManager: - """Handles CloudFormation Services""" - - def __init__(self, module): - self.module = module - self.client = module.client('cloudformation') - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stacks_with_backoff(self, **kwargs): - paginator = self.client.get_paginator('describe_stacks') - return paginator.paginate(**kwargs).build_full_result()['Stacks'] - - def describe_stacks(self, stack_name=None): - try: - kwargs = {'StackName': stack_name} if stack_name else {} - response = self.describe_stacks_with_backoff(**kwargs) - if response is not None: - return response - self.module.fail_json(msg="Error describing stack(s) - an empty response was returned") - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - if 'does not exist' in e.response['Error']['Message']: - # missing stack, don't bail. - return {} - self.module.fail_json_aws(e, msg="Error describing stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def list_stack_resources_with_backoff(self, stack_name): - paginator = self.client.get_paginator('list_stack_resources') - return paginator.paginate(StackName=stack_name).build_full_result()['StackResourceSummaries'] - - def list_stack_resources(self, stack_name): - try: - return self.list_stack_resources_with_backoff(stack_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error listing stack resources for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stack_events_with_backoff(self, stack_name): - paginator = self.client.get_paginator('describe_stack_events') - return paginator.paginate(StackName=stack_name).build_full_result()['StackEvents'] - - def describe_stack_events(self, stack_name): - try: - return self.describe_stack_events_with_backoff(stack_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error listing stack events for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def list_stack_change_sets_with_backoff(self, stack_name): - paginator = self.client.get_paginator('list_change_sets') - return paginator.paginate(StackName=stack_name).build_full_result()['Summaries'] - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stack_change_set_with_backoff(self, **kwargs): - paginator = self.client.get_paginator('describe_change_set') - return paginator.paginate(**kwargs).build_full_result() - - def describe_stack_change_sets(self, stack_name): - changes = [] - try: - change_sets = self.list_stack_change_sets_with_backoff(stack_name) - for item in change_sets: - changes.append(self.describe_stack_change_set_with_backoff( - StackName=stack_name, - ChangeSetName=item['ChangeSetName'])) - return changes - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error describing stack change sets for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def get_stack_policy_with_backoff(self, stack_name): - return self.client.get_stack_policy(StackName=stack_name) - - def get_stack_policy(self, stack_name): - try: - response = self.get_stack_policy_with_backoff(stack_name) - stack_policy = response.get('StackPolicyBody') - if stack_policy: - return json.loads(stack_policy) - return dict() - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error getting stack policy for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def get_template_with_backoff(self, stack_name): - return self.client.get_template(StackName=stack_name) - - def get_template(self, stack_name): - try: - response = self.get_template_with_backoff(stack_name) - return response.get('TemplateBody') - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error getting stack template for stack " + stack_name) - - -def to_dict(items, key, value): - ''' Transforms a list of items to a Key/Value dictionary ''' - if items: - return dict(zip([i.get(key) for i in items], [i.get(value) for i in items])) - else: - return dict() - - -def main(): - argument_spec = dict( - stack_name=dict(), - all_facts=dict(required=False, default=False, type='bool'), - stack_policy=dict(required=False, default=False, type='bool'), - stack_events=dict(required=False, default=False, type='bool'), - stack_resources=dict(required=False, default=False, type='bool'), - stack_template=dict(required=False, default=False, type='bool'), - stack_change_sets=dict(required=False, default=False, type='bool'), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - is_old_facts = module._name == 'cloudformation_facts' - if is_old_facts: - module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', " - "and the renamed one no longer returns ansible_facts", - version='2.13', collection_name='ansible.builtin') - - service_mgr = CloudFormationServiceManager(module) - - if is_old_facts: - result = {'ansible_facts': {'cloudformation': {}}} - else: - result = {'cloudformation': {}} - - for stack_description in service_mgr.describe_stacks(module.params.get('stack_name')): - facts = {'stack_description': stack_description} - stack_name = stack_description.get('StackName') - - # Create stack output and stack parameter dictionaries - if facts['stack_description']: - facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue') - facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'), - 'ParameterKey', 'ParameterValue') - facts['stack_tags'] = boto3_tag_list_to_ansible_dict(facts['stack_description'].get('Tags')) - - # Create optional stack outputs - all_facts = module.params.get('all_facts') - if all_facts or module.params.get('stack_resources'): - facts['stack_resource_list'] = service_mgr.list_stack_resources(stack_name) - facts['stack_resources'] = to_dict(facts.get('stack_resource_list'), - 'LogicalResourceId', 'PhysicalResourceId') - if all_facts or module.params.get('stack_template'): - facts['stack_template'] = service_mgr.get_template(stack_name) - if all_facts or module.params.get('stack_policy'): - facts['stack_policy'] = service_mgr.get_stack_policy(stack_name) - if all_facts or module.params.get('stack_events'): - facts['stack_events'] = service_mgr.describe_stack_events(stack_name) - if all_facts or module.params.get('stack_change_sets'): - facts['stack_change_sets'] = service_mgr.describe_stack_change_sets(stack_name) - - if is_old_facts: - result['ansible_facts']['cloudformation'][stack_name] = facts - else: - result['cloudformation'][stack_name] = camel_dict_to_snake_dict(facts, ignore_list=('stack_outputs', - 'stack_parameters', - 'stack_policy', - 'stack_resources', - 'stack_tags', - 'stack_template')) - - module.exit_json(changed=False, **result) - - -if __name__ == '__main__': - main()