1#!/usr/local/bin/python3.8 2 3# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9 10DOCUMENTATION = r''' 11--- 12module: aws_config_aggregator 13version_added: 1.0.0 14short_description: Manage AWS Config aggregations across multiple accounts 15description: 16 - Module manages AWS Config resources 17requirements: [ 'botocore', 'boto3' ] 18author: 19 - "Aaron Smith (@slapula)" 20options: 21 name: 22 description: 23 - The name of the AWS Config resource. 24 required: true 25 type: str 26 state: 27 description: 28 - Whether the Config rule should be present or absent. 29 default: present 30 choices: ['present', 'absent'] 31 type: str 32 account_sources: 33 description: 34 - Provides a list of source accounts and regions to be aggregated. 35 suboptions: 36 account_ids: 37 description: 38 - A list of 12-digit account IDs of accounts being aggregated. 39 type: list 40 elements: str 41 aws_regions: 42 description: 43 - A list of source regions being aggregated. 44 type: list 45 elements: str 46 all_aws_regions: 47 description: 48 - If true, aggregate existing AWS Config regions and future regions. 49 type: bool 50 type: list 51 elements: dict 52 required: true 53 organization_source: 54 description: 55 - The region authorized to collect aggregated data. 56 suboptions: 57 role_arn: 58 description: 59 - ARN of the IAM role used to retrieve AWS Organization details associated with the aggregator account. 60 type: str 61 aws_regions: 62 description: 63 - The source regions being aggregated. 64 type: list 65 elements: str 66 all_aws_regions: 67 description: 68 - If true, aggregate existing AWS Config regions and future regions. 69 type: bool 70 type: dict 71 required: true 72extends_documentation_fragment: 73- amazon.aws.aws 74- amazon.aws.ec2 75 76''' 77 78EXAMPLES = r''' 79- name: Create cross-account aggregator 80 community.aws.aws_config_aggregator: 81 name: test_config_rule 82 state: present 83 account_sources: 84 account_ids: 85 - 1234567890 86 - 0123456789 87 - 9012345678 88 all_aws_regions: yes 89''' 90 91RETURN = r'''#''' 92 93 94try: 95 import botocore 96except ImportError: 97 pass # handled by AnsibleAWSModule 98 99from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code 100from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict 101 102 103def resource_exists(client, module, params): 104 try: 105 aggregator = client.describe_configuration_aggregators( 106 ConfigurationAggregatorNames=[params['ConfigurationAggregatorName']] 107 ) 108 return aggregator['ConfigurationAggregators'][0] 109 except is_boto3_error_code('NoSuchConfigurationAggregatorException'): 110 return 111 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except 112 module.fail_json_aws(e) 113 114 115def create_resource(client, module, params, result): 116 try: 117 client.put_configuration_aggregator( 118 ConfigurationAggregatorName=params['ConfigurationAggregatorName'], 119 AccountAggregationSources=params['AccountAggregationSources'], 120 OrganizationAggregationSource=params['OrganizationAggregationSource'] 121 ) 122 result['changed'] = True 123 result['aggregator'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) 124 return result 125 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 126 module.fail_json_aws(e, msg="Couldn't create AWS Config configuration aggregator") 127 128 129def update_resource(client, module, params, result): 130 current_params = client.describe_configuration_aggregators( 131 ConfigurationAggregatorNames=[params['ConfigurationAggregatorName']] 132 ) 133 134 del current_params['ConfigurationAggregatorArn'] 135 del current_params['CreationTime'] 136 del current_params['LastUpdatedTime'] 137 138 if params != current_params['ConfigurationAggregators'][0]: 139 try: 140 client.put_configuration_aggregator( 141 ConfigurationAggregatorName=params['ConfigurationAggregatorName'], 142 AccountAggregationSources=params['AccountAggregationSources'], 143 OrganizationAggregationSource=params['OrganizationAggregationSource'] 144 ) 145 result['changed'] = True 146 result['aggregator'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) 147 return result 148 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 149 module.fail_json_aws(e, msg="Couldn't create AWS Config configuration aggregator") 150 151 152def delete_resource(client, module, params, result): 153 try: 154 client.delete_configuration_aggregator( 155 ConfigurationAggregatorName=params['ConfigurationAggregatorName'] 156 ) 157 result['changed'] = True 158 return result 159 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 160 module.fail_json_aws(e, msg="Couldn't delete AWS Config configuration aggregator") 161 162 163def main(): 164 module = AnsibleAWSModule( 165 argument_spec={ 166 'name': dict(type='str', required=True), 167 'state': dict(type='str', choices=['present', 'absent'], default='present'), 168 'account_sources': dict(type='list', required=True, elements='dict'), 169 'organization_source': dict(type='dict', required=True) 170 }, 171 supports_check_mode=False, 172 ) 173 174 result = { 175 'changed': False 176 } 177 178 name = module.params.get('name') 179 state = module.params.get('state') 180 181 params = {} 182 if name: 183 params['ConfigurationAggregatorName'] = name 184 params['AccountAggregationSources'] = [] 185 if module.params.get('account_sources'): 186 for i in module.params.get('account_sources'): 187 tmp_dict = {} 188 if i.get('account_ids'): 189 tmp_dict['AccountIds'] = i.get('account_ids') 190 if i.get('aws_regions'): 191 tmp_dict['AwsRegions'] = i.get('aws_regions') 192 if i.get('all_aws_regions') is not None: 193 tmp_dict['AllAwsRegions'] = i.get('all_aws_regions') 194 params['AccountAggregationSources'].append(tmp_dict) 195 if module.params.get('organization_source'): 196 params['OrganizationAggregationSource'] = {} 197 if module.params.get('organization_source').get('role_arn'): 198 params['OrganizationAggregationSource'].update({ 199 'RoleArn': module.params.get('organization_source').get('role_arn') 200 }) 201 if module.params.get('organization_source').get('aws_regions'): 202 params['OrganizationAggregationSource'].update({ 203 'AwsRegions': module.params.get('organization_source').get('aws_regions') 204 }) 205 if module.params.get('organization_source').get('all_aws_regions') is not None: 206 params['OrganizationAggregationSource'].update({ 207 'AllAwsRegions': module.params.get('organization_source').get('all_aws_regions') 208 }) 209 210 client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) 211 212 resource_status = resource_exists(client, module, params) 213 214 if state == 'present': 215 if not resource_status: 216 create_resource(client, module, params, result) 217 else: 218 update_resource(client, module, params, result) 219 220 if state == 'absent': 221 if resource_status: 222 delete_resource(client, module, params, result) 223 224 module.exit_json(changed=result['changed']) 225 226 227if __name__ == '__main__': 228 main() 229