#!/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') 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()