From 380c43de4ee09caa4501edb7b543f94145adcf35 Mon Sep 17 00:00:00 2001 From: mzizzi Date: Wed, 9 Aug 2017 19:06:40 -0400 Subject: [PATCH] cloudformation_facts: describe all stacks by default * cloudformation_facts describe all stacks by default * cloudformation_facts jittered backoff / retries * cloudformation_facts stack_name use default arg_spec * cloudformation_facts bugfix broken notification_arns output * cloudformation_facts add simplified "stack_tags" output * CloudFormationServiceManager.describe_stacks default args --- .../cloud/amazon/cloudformation_facts.py | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/lib/ansible/modules/cloud/amazon/cloudformation_facts.py b/lib/ansible/modules/cloud/amazon/cloudformation_facts.py index 9795ddf6cbb..84b49b2a3d6 100644 --- a/lib/ansible/modules/cloud/amazon/cloudformation_facts.py +++ b/lib/ansible/modules/cloud/amazon/cloudformation_facts.py @@ -33,8 +33,9 @@ author: Justin Menga (@jmenga) options: stack_name: description: - - The name or id of the CloudFormation stack - required: true + - The name or id of the CloudFormation stack. Gathers facts for all stacks by default. + required: false + default: null all_facts: description: - Get all stack information for the stack @@ -151,7 +152,8 @@ try: except ImportError: HAS_BOTO3 = False -from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, boto3_conn, camel_dict_to_snake_dict +from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, boto3_conn, camel_dict_to_snake_dict, \ + AWSRetry, boto3_tag_list_to_ansible_dict from ansible.module_utils.basic import AnsibleModule from functools import partial import json @@ -169,20 +171,27 @@ class CloudFormationServiceManager: self.client = boto3_conn(module, conn_type='client', resource='cloudformation', region=region, endpoint=ec2_url, **aws_connect_kwargs) + backoff_wrapper = AWSRetry.jittered_backoff(retries=10, delay=3, max_delay=30) + self.client.describe_stacks = backoff_wrapper(self.client.describe_stacks) + self.client.list_stack_resources = backoff_wrapper(self.client.list_stack_resources) + self.client.describe_stack_events = backoff_wrapper(self.client.describe_stack_events) + self.client.get_stack_policy = backoff_wrapper(self.client.get_stack_policy) + self.client.get_template = backoff_wrapper(self.client.get_template) except botocore.exceptions.NoRegionError: self.module.fail_json(msg="Region must be specified as a parameter, in AWS_DEFAULT_REGION environment variable or in boto configuration file") except Exception as e: self.module.fail_json(msg="Can't establish connection - " + str(e), exception=traceback.format_exc()) - def describe_stack(self, stack_name): + def describe_stacks(self, stack_name=None): try: - func = partial(self.client.describe_stacks,StackName=stack_name) + kwargs = {'StackName': stack_name} if stack_name else {} + func = partial(self.client.describe_stacks, **kwargs) response = self.paginated_response(func, 'Stacks') if response: - return response[0] - self.module.fail_json(msg="Error describing stack - an empty response was returned") + return response + self.module.fail_json(msg="Error describing stack(s) - an empty response was returned") except Exception as e: - self.module.fail_json(msg="Error describing stack - " + str(e), exception=traceback.format_exc()) + self.module.fail_json(msg="Error describing stack(s) - " + str(e), exception=traceback.format_exc()) def list_stack_resources(self, stack_name): try: @@ -240,7 +249,7 @@ def to_dict(items, key, value): def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - stack_name=dict(required=True, type='str' ), + 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'), @@ -253,36 +262,36 @@ def main(): if not HAS_BOTO3: module.fail_json(msg='boto3 is required.') - # Describe the stack service_mgr = CloudFormationServiceManager(module) - stack_name = module.params.get('stack_name') - result = { - 'ansible_facts': { 'cloudformation': { stack_name:{} } } - } - facts = result['ansible_facts']['cloudformation'][stack_name] - facts['stack_description'] = service_mgr.describe_stack(stack_name) - # 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') + result = {'ansible_facts': {'cloudformation': {}}} - # normalize stack description API output - facts['stack_description'] = camel_dict_to_snake_dict(facts['stack_description']) - # camel2snake doesn't handle NotificationARNs properly, so let's fix that - facts['stack_description']['notification_arns'] = facts['stack_description'].pop('notification_ar_ns', []) + 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 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) + # 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')) + + # normalize stack description API output + facts['stack_description'] = camel_dict_to_snake_dict(facts['stack_description']) + + # 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) + + result['ansible_facts']['cloudformation'][stack_name] = facts result['changed'] = False module.exit_json(**result)