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