1#!/usr/local/bin/python3.8
2# Copyright: (c) 2018, Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8
9DOCUMENTATION = r'''
10---
11module: cloudformation_stack_set
12version_added: 1.0.0
13short_description: Manage groups of CloudFormation stacks
14description:
15     - Launches/updates/deletes AWS CloudFormation Stack Sets.
16notes:
17     - To make an individual stack, you want the M(amazon.aws.cloudformation) module.
18options:
19  name:
20    description:
21      - Name of the CloudFormation stack set.
22    required: true
23    type: str
24  description:
25    description:
26      - A description of what this stack set creates.
27    type: str
28  parameters:
29    description:
30      - A list of hashes of all the template variables for the stack. The value can be a string or a dict.
31      - Dict can be used to set additional template parameter attributes like UsePreviousValue (see example).
32    default: {}
33    type: dict
34  state:
35    description:
36      - If I(state=present), stack will be created.  If I(state=present) and if stack exists and template has changed, it will be updated.
37        If I(state=absent), stack will be removed.
38    default: present
39    choices: [ present, absent ]
40    type: str
41  template:
42    description:
43      - The local path of the CloudFormation template.
44      - This must be the full path to the file, relative to the working directory. If using roles this may look
45        like C(roles/cloudformation/files/cloudformation-example.json).
46      - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
47        must be specified (but only one of them).
48      - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url)
49        are specified, the previous template will be reused.
50    type: path
51  template_body:
52    description:
53      - Template body. Use this to pass in the actual body of the CloudFormation template.
54      - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
55        must be specified (but only one of them).
56      - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url)
57        are specified, the previous template will be reused.
58    type: str
59  template_url:
60    description:
61      - Location of file containing the template body.
62      - The URL must point to a template (max size 307,200 bytes) located in an S3 bucket in the same region
63        as the stack.
64      - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
65        must be specified (but only one of them).
66      - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url)
67        are specified, the previous template will be reused.
68    type: str
69  purge_stacks:
70    description:
71    - Only applicable when I(state=absent). Sets whether, when deleting a stack set, the stack instances should also be deleted.
72    - By default, instances will be deleted. To keep stacks when stack set is deleted set I(purge_stacks=false).
73    type: bool
74    default: true
75  wait:
76    description:
77    - Whether or not to wait for stack operation to complete. This includes waiting for stack instances to reach UPDATE_COMPLETE status.
78    - If you choose not to wait, this module will not notify when stack operations fail because it will not wait for them to finish.
79    type: bool
80    default: false
81  wait_timeout:
82    description:
83    - How long to wait (in seconds) for stacks to complete create/update/delete operations.
84    default: 900
85    type: int
86  capabilities:
87    description:
88    - Capabilities allow stacks to create and modify IAM resources, which may include adding users or roles.
89    - Currently the only available values are 'CAPABILITY_IAM' and 'CAPABILITY_NAMED_IAM'. Either or both may be provided.
90    - >
91        The following resources require that one or both of these parameters is specified: AWS::IAM::AccessKey,
92        AWS::IAM::Group, AWS::IAM::InstanceProfile, AWS::IAM::Policy, AWS::IAM::Role, AWS::IAM::User, AWS::IAM::UserToGroupAddition
93    type: list
94    elements: str
95    choices:
96    - 'CAPABILITY_IAM'
97    - 'CAPABILITY_NAMED_IAM'
98  regions:
99    description:
100    - A list of AWS regions to create instances of a stack in. The I(region) parameter chooses where the Stack Set is created, and I(regions)
101      specifies the region for stack instances.
102    - At least one region must be specified to create a stack set. On updates, if fewer regions are specified only the specified regions will
103      have their stack instances updated.
104    type: list
105    elements: str
106  accounts:
107    description:
108    - A list of AWS accounts in which to create instance of CloudFormation stacks.
109    - At least one region must be specified to create a stack set. On updates, if fewer regions are specified only the specified regions will
110      have their stack instances updated.
111    type: list
112    elements: str
113  administration_role_arn:
114    description:
115    - ARN of the administration role, meaning the role that CloudFormation Stack Sets use to assume the roles in your child accounts.
116    - This defaults to C(arn:aws:iam::{{ account ID }}:role/AWSCloudFormationStackSetAdministrationRole) where C({{ account ID }}) is replaced with the
117      account number of the current IAM role/user/STS credentials.
118    aliases:
119    - admin_role_arn
120    - admin_role
121    - administration_role
122    type: str
123  execution_role_name:
124    description:
125    - ARN of the execution role, meaning the role that CloudFormation Stack Sets assumes in your child accounts.
126    - This MUST NOT be an ARN, and the roles must exist in each child account specified.
127    - The default name for the execution role is C(AWSCloudFormationStackSetExecutionRole)
128    aliases:
129    - exec_role_name
130    - exec_role
131    - execution_role
132    type: str
133  tags:
134    description:
135      - Dictionary of tags to associate with stack and its resources during stack creation.
136      - Can be updated later, updating tags removes previous entries.
137    type: dict
138  failure_tolerance:
139    description:
140    - Settings to change what is considered "failed" when running stack instance updates, and how many to do at a time.
141    type: dict
142    suboptions:
143      fail_count:
144        description:
145        - The number of accounts, per region, for which this operation can fail before CloudFormation
146          stops the operation in that region.
147        - You must specify one of I(fail_count) and I(fail_percentage).
148        type: int
149      fail_percentage:
150        type: int
151        description:
152        - The percentage of accounts, per region, for which this stack operation can fail before CloudFormation
153          stops the operation in that region.
154        - You must specify one of I(fail_count) and I(fail_percentage).
155      parallel_percentage:
156        type: int
157        description:
158        - The maximum percentage of accounts in which to perform this operation at one time.
159        - You must specify one of I(parallel_count) and I(parallel_percentage).
160        - Note that this setting lets you specify the maximum for operations.
161          For large deployments, under certain circumstances the actual percentage may be lower.
162      parallel_count:
163        type: int
164        description:
165        - The maximum number of accounts in which to perform this operation at one time.
166        - I(parallel_count) may be at most one more than the I(fail_count).
167        - You must specify one of I(parallel_count) and I(parallel_percentage).
168        - Note that this setting lets you specify the maximum for operations.
169          For large deployments, under certain circumstances the actual count may be lower.
170
171author: "Ryan Scott Brown (@ryansb)"
172extends_documentation_fragment:
173- amazon.aws.aws
174- amazon.aws.ec2
175
176requirements: [ boto3>=1.6, botocore>=1.10.26 ]
177'''
178
179EXAMPLES = r'''
180- name: Create a stack set with instances in two accounts
181  community.aws.cloudformation_stack_set:
182    name: my-stack
183    description: Test stack in two accounts
184    state: present
185    template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template
186    accounts: [1234567890, 2345678901]
187    regions:
188    - us-east-1
189
190- name: on subsequent calls, templates are optional but parameters and tags can be altered
191  community.aws.cloudformation_stack_set:
192    name: my-stack
193    state: present
194    parameters:
195      InstanceName: my_stacked_instance
196    tags:
197      foo: bar
198      test: stack
199    accounts: [1234567890, 2345678901]
200    regions:
201    - us-east-1
202
203- name: The same type of update, but wait for the update to complete in all stacks
204  community.aws.cloudformation_stack_set:
205    name: my-stack
206    state: present
207    wait: true
208    parameters:
209      InstanceName: my_restacked_instance
210    tags:
211      foo: bar
212      test: stack
213    accounts: [1234567890, 2345678901]
214    regions:
215    - us-east-1
216'''
217
218RETURN = r'''
219operations_log:
220  type: list
221  description: Most recent events in CloudFormation's event log. This may be from a previous run in some cases.
222  returned: always
223  sample:
224  - action: CREATE
225    creation_timestamp: '2018-06-18T17:40:46.372000+00:00'
226    end_timestamp: '2018-06-18T17:41:24.560000+00:00'
227    operation_id: Ansible-StackInstance-Create-0ff2af5b-251d-4fdb-8b89-1ee444eba8b8
228    status: FAILED
229    stack_instances:
230    - account: '1234567890'
231      region: us-east-1
232      stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929
233      status: OUTDATED
234      status_reason: Account 1234567890 should have 'AWSCloudFormationStackSetAdministrationRole' role with trust relationship to CloudFormation service.
235
236operations:
237  description: All operations initiated by this run of the cloudformation_stack_set module
238  returned: always
239  type: list
240  sample:
241  - action: CREATE
242    administration_role_arn: arn:aws:iam::1234567890:role/AWSCloudFormationStackSetAdministrationRole
243    creation_timestamp: '2018-06-18T17:40:46.372000+00:00'
244    end_timestamp: '2018-06-18T17:41:24.560000+00:00'
245    execution_role_name: AWSCloudFormationStackSetExecutionRole
246    operation_id: Ansible-StackInstance-Create-0ff2af5b-251d-4fdb-8b89-1ee444eba8b8
247    operation_preferences:
248      region_order:
249      - us-east-1
250      - us-east-2
251    stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929
252    status: FAILED
253stack_instances:
254  description: CloudFormation stack instances that are members of this stack set. This will also include their region and account ID.
255  returned: state == present
256  type: list
257  sample:
258    - account: '1234567890'
259      region: us-east-1
260      stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929
261      status: OUTDATED
262      status_reason: >
263        Account 1234567890 should have 'AWSCloudFormationStackSetAdministrationRole' role with trust relationship to CloudFormation service.
264    - account: '1234567890'
265      region: us-east-2
266      stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929
267      status: OUTDATED
268      status_reason: Cancelled since failure tolerance has exceeded
269stack_set:
270  type: dict
271  description: Facts about the currently deployed stack set, its parameters, and its tags
272  returned: state == present
273  sample:
274    administration_role_arn: arn:aws:iam::1234567890:role/AWSCloudFormationStackSetAdministrationRole
275    capabilities: []
276    description: test stack PRIME
277    execution_role_name: AWSCloudFormationStackSetExecutionRole
278    parameters: []
279    stack_set_arn: arn:aws:cloudformation:us-east-1:1234567890:stackset/TestStackPrime:19f3f684-aae9-467-ba36-e09f92cf5929
280    stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929
281    stack_set_name: TestStackPrime
282    status: ACTIVE
283    tags:
284      Some: Thing
285      an: other
286    template_body: |
287      AWSTemplateFormatVersion: "2010-09-09"
288      Parameters: {}
289      Resources:
290        Bukkit:
291          Type: "AWS::S3::Bucket"
292          Properties: {}
293        other:
294          Type: "AWS::SNS::Topic"
295          Properties: {}
296
297'''  # NOQA
298
299import datetime
300import itertools
301import time
302import uuid
303
304try:
305    from botocore.exceptions import ClientError, BotoCoreError
306except ImportError:
307    # handled by AnsibleAWSModule
308    pass
309
310from ansible.module_utils._text import to_native
311from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
312
313from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
314from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
315from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
316from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
317from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
318
319
320def create_stack_set(module, stack_params, cfn):
321    try:
322        cfn.create_stack_set(aws_retry=True, **stack_params)
323        return await_stack_set_exists(cfn, stack_params['StackSetName'])
324    except (ClientError, BotoCoreError) as err:
325        module.fail_json_aws(err, msg="Failed to create stack set {0}.".format(stack_params.get('StackSetName')))
326
327
328def update_stack_set(module, stack_params, cfn):
329    # if the state is present and the stack already exists, we try to update it.
330    # AWS will tell us if the stack template and parameters are the same and
331    # don't need to be updated.
332    try:
333        cfn.update_stack_set(**stack_params)
334    except is_boto3_error_code('StackSetNotFound') as err:  # pylint: disable=duplicate-except
335        module.fail_json_aws(err, msg="Failed to find stack set. Check the name & region.")
336    except is_boto3_error_code('StackInstanceNotFound') as err:  # pylint: disable=duplicate-except
337        module.fail_json_aws(err, msg="One or more stack instances were not found for this stack set. Double check "
338                             "the `accounts` and `regions` parameters.")
339    except is_boto3_error_code('OperationInProgressException') as err:  # pylint: disable=duplicate-except
340        module.fail_json_aws(
341            err, msg="Another operation is already in progress on this stack set - please try again later. When making "
342            "multiple cloudformation_stack_set calls, it's best to enable `wait: yes` to avoid unfinished op errors.")
343    except (ClientError, BotoCoreError) as err:  # pylint: disable=duplicate-except
344        module.fail_json_aws(err, msg="Could not update stack set.")
345    if module.params.get('wait'):
346        await_stack_set_operation(
347            module, cfn, operation_id=stack_params['OperationId'],
348            stack_set_name=stack_params['StackSetName'],
349            max_wait=module.params.get('wait_timeout'),
350        )
351
352    return True
353
354
355def compare_stack_instances(cfn, stack_set_name, accounts, regions):
356    instance_list = cfn.list_stack_instances(
357        aws_retry=True,
358        StackSetName=stack_set_name,
359    )['Summaries']
360    desired_stack_instances = set(itertools.product(accounts, regions))
361    existing_stack_instances = set((i['Account'], i['Region']) for i in instance_list)
362    # new stacks, existing stacks, unspecified stacks
363    return (desired_stack_instances - existing_stack_instances), existing_stack_instances, (existing_stack_instances - desired_stack_instances)
364
365
366@AWSRetry.backoff(tries=3, delay=4)
367def stack_set_facts(cfn, stack_set_name):
368    try:
369        ss = cfn.describe_stack_set(StackSetName=stack_set_name)['StackSet']
370        ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags'])
371        return ss
372    except cfn.exceptions.from_code('StackSetNotFound'):
373        # Return None if the stack doesn't exist
374        return
375
376
377def await_stack_set_operation(module, cfn, stack_set_name, operation_id, max_wait):
378    wait_start = datetime.datetime.now()
379    operation = None
380    for i in range(max_wait // 15):
381        try:
382            operation = cfn.describe_stack_set_operation(StackSetName=stack_set_name, OperationId=operation_id)
383            if operation['StackSetOperation']['Status'] not in ('RUNNING', 'STOPPING'):
384                # Stack set has completed operation
385                break
386        except is_boto3_error_code('StackSetNotFound'):  # pylint: disable=duplicate-except
387            pass
388        except is_boto3_error_code('OperationNotFound'):  # pylint: disable=duplicate-except
389            pass
390        time.sleep(15)
391
392    if operation and operation['StackSetOperation']['Status'] not in ('FAILED', 'STOPPED'):
393        await_stack_instance_completion(
394            module, cfn,
395            stack_set_name=stack_set_name,
396            # subtract however long we waited already
397            max_wait=int(max_wait - (datetime.datetime.now() - wait_start).total_seconds()),
398        )
399    elif operation and operation['StackSetOperation']['Status'] in ('FAILED', 'STOPPED'):
400        pass
401    else:
402        module.warn(
403            "Timed out waiting for operation {0} on stack set {1} after {2} seconds. Returning unfinished operation".format(
404                operation_id, stack_set_name, max_wait
405            )
406        )
407
408
409def await_stack_instance_completion(module, cfn, stack_set_name, max_wait):
410    to_await = None
411    for i in range(max_wait // 15):
412        try:
413            stack_instances = cfn.list_stack_instances(StackSetName=stack_set_name)
414            to_await = [inst for inst in stack_instances['Summaries']
415                        if inst['Status'] != 'CURRENT']
416            if not to_await:
417                return stack_instances['Summaries']
418        except is_boto3_error_code('StackSetNotFound'):  # pylint: disable=duplicate-except
419            # this means the deletion beat us, or the stack set is not yet propagated
420            pass
421        time.sleep(15)
422
423    module.warn(
424        "Timed out waiting for stack set {0} instances {1} to complete after {2} seconds. Returning unfinished operation".format(
425            stack_set_name, ', '.join(s['StackId'] for s in to_await), max_wait
426        )
427    )
428
429
430def await_stack_set_exists(cfn, stack_set_name):
431    # AWSRetry will retry on `StackSetNotFound` errors for us
432    ss = cfn.describe_stack_set(StackSetName=stack_set_name, aws_retry=True)['StackSet']
433    ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags'])
434    return camel_dict_to_snake_dict(ss, ignore_list=('Tags',))
435
436
437def describe_stack_tree(module, stack_set_name, operation_ids=None):
438    jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=5, delay=3, max_delay=5, catch_extra_error_codes=['StackSetNotFound'])
439    cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator)
440    result = dict()
441    result['stack_set'] = camel_dict_to_snake_dict(
442        cfn.describe_stack_set(
443            StackSetName=stack_set_name,
444            aws_retry=True,
445        )['StackSet']
446    )
447    result['stack_set']['tags'] = boto3_tag_list_to_ansible_dict(result['stack_set']['tags'])
448    result['operations_log'] = sorted(
449        camel_dict_to_snake_dict(
450            cfn.list_stack_set_operations(
451                StackSetName=stack_set_name,
452                aws_retry=True,
453            )
454        )['summaries'],
455        key=lambda x: x['creation_timestamp']
456    )
457    result['stack_instances'] = sorted(
458        [
459            camel_dict_to_snake_dict(i) for i in
460            cfn.list_stack_instances(StackSetName=stack_set_name)['Summaries']
461        ],
462        key=lambda i: i['region'] + i['account']
463    )
464
465    if operation_ids:
466        result['operations'] = []
467        for op_id in operation_ids:
468            try:
469                result['operations'].append(camel_dict_to_snake_dict(
470                    cfn.describe_stack_set_operation(
471                        StackSetName=stack_set_name,
472                        OperationId=op_id,
473                    )['StackSetOperation']
474                ))
475            except is_boto3_error_code('OperationNotFoundException'):  # pylint: disable=duplicate-except
476                pass
477    return result
478
479
480def get_operation_preferences(module):
481    params = dict()
482    if module.params.get('regions'):
483        params['RegionOrder'] = list(module.params['regions'])
484    for param, api_name in {
485        'fail_count': 'FailureToleranceCount',
486        'fail_percentage': 'FailureTolerancePercentage',
487        'parallel_percentage': 'MaxConcurrentPercentage',
488        'parallel_count': 'MaxConcurrentCount',
489    }.items():
490        if module.params.get('failure_tolerance', {}).get(param):
491            params[api_name] = module.params.get('failure_tolerance', {}).get(param)
492    return params
493
494
495def main():
496    argument_spec = dict(
497        name=dict(required=True),
498        description=dict(),
499        wait=dict(type='bool', default=False),
500        wait_timeout=dict(type='int', default=900),
501        state=dict(default='present', choices=['present', 'absent']),
502        purge_stacks=dict(type='bool', default=True),
503        parameters=dict(type='dict', default={}),
504        template=dict(type='path'),
505        template_url=dict(),
506        template_body=dict(),
507        capabilities=dict(type='list', elements='str', choices=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']),
508        regions=dict(type='list', elements='str'),
509        accounts=dict(type='list', elements='str'),
510        failure_tolerance=dict(
511            type='dict',
512            default={},
513            options=dict(
514                fail_count=dict(type='int'),
515                fail_percentage=dict(type='int'),
516                parallel_percentage=dict(type='int'),
517                parallel_count=dict(type='int'),
518            ),
519            mutually_exclusive=[
520                ['fail_count', 'fail_percentage'],
521                ['parallel_count', 'parallel_percentage'],
522            ],
523        ),
524        administration_role_arn=dict(aliases=['admin_role_arn', 'administration_role', 'admin_role']),
525        execution_role_name=dict(aliases=['execution_role', 'exec_role', 'exec_role_name']),
526        tags=dict(type='dict'),
527    )
528
529    module = AnsibleAWSModule(
530        argument_spec=argument_spec,
531        mutually_exclusive=[['template_url', 'template', 'template_body']],
532        supports_check_mode=True
533    )
534    if not (module.boto3_at_least('1.6.0') and module.botocore_at_least('1.10.26')):
535        module.fail_json(msg="Boto3 or botocore version is too low. This module requires at least boto3 1.6 and botocore 1.10.26")
536
537    # Wrap the cloudformation client methods that this module uses with
538    # automatic backoff / retry for throttling error codes
539    jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=10, delay=3, max_delay=30, catch_extra_error_codes=['StackSetNotFound'])
540    cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator)
541    existing_stack_set = stack_set_facts(cfn, module.params['name'])
542
543    operation_uuid = to_native(uuid.uuid4())
544    operation_ids = []
545    # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around.
546    stack_params = {}
547    state = module.params['state']
548    if state == 'present' and not module.params['accounts']:
549        module.fail_json(
550            msg="Can't create a stack set without choosing at least one account. "
551                "To get the ID of the current account, use the aws_caller_info module."
552        )
553
554    module.params['accounts'] = [to_native(a) for a in module.params['accounts']]
555
556    stack_params['StackSetName'] = module.params['name']
557    if module.params.get('description'):
558        stack_params['Description'] = module.params['description']
559
560    if module.params.get('capabilities'):
561        stack_params['Capabilities'] = module.params['capabilities']
562
563    if module.params['template'] is not None:
564        with open(module.params['template'], 'r') as tpl:
565            stack_params['TemplateBody'] = tpl.read()
566    elif module.params['template_body'] is not None:
567        stack_params['TemplateBody'] = module.params['template_body']
568    elif module.params['template_url'] is not None:
569        stack_params['TemplateURL'] = module.params['template_url']
570    else:
571        # no template is provided, but if the stack set exists already, we can use the existing one.
572        if existing_stack_set:
573            stack_params['UsePreviousTemplate'] = True
574        else:
575            module.fail_json(
576                msg="The Stack Set {0} does not exist, and no template was provided. Provide one of `template`, "
577                    "`template_body`, or `template_url`".format(module.params['name'])
578            )
579
580    stack_params['Parameters'] = []
581    for k, v in module.params['parameters'].items():
582        if isinstance(v, dict):
583            # set parameter based on a dict to allow additional CFN Parameter Attributes
584            param = dict(ParameterKey=k)
585
586            if 'value' in v:
587                param['ParameterValue'] = to_native(v['value'])
588
589            if 'use_previous_value' in v and bool(v['use_previous_value']):
590                param['UsePreviousValue'] = True
591                param.pop('ParameterValue', None)
592
593            stack_params['Parameters'].append(param)
594        else:
595            # allow default k/v configuration to set a template parameter
596            stack_params['Parameters'].append({'ParameterKey': k, 'ParameterValue': str(v)})
597
598    if module.params.get('tags') and isinstance(module.params.get('tags'), dict):
599        stack_params['Tags'] = ansible_dict_to_boto3_tag_list(module.params['tags'])
600
601    if module.params.get('administration_role_arn'):
602        # TODO loosen the semantics here to autodetect the account ID and build the ARN
603        stack_params['AdministrationRoleARN'] = module.params['administration_role_arn']
604    if module.params.get('execution_role_name'):
605        stack_params['ExecutionRoleName'] = module.params['execution_role_name']
606
607    result = {}
608
609    if module.check_mode:
610        if state == 'absent' and existing_stack_set:
611            module.exit_json(changed=True, msg='Stack set would be deleted', meta=[])
612        elif state == 'absent' and not existing_stack_set:
613            module.exit_json(changed=False, msg='Stack set doesn\'t exist', meta=[])
614        elif state == 'present' and not existing_stack_set:
615            module.exit_json(changed=True, msg='New stack set would be created', meta=[])
616        elif state == 'present' and existing_stack_set:
617            new_stacks, existing_stacks, unspecified_stacks = compare_stack_instances(
618                cfn,
619                module.params['name'],
620                module.params['accounts'],
621                module.params['regions'],
622            )
623            if new_stacks:
624                module.exit_json(changed=True, msg='New stack instance(s) would be created', meta=[])
625            elif unspecified_stacks and module.params.get('purge_stack_instances'):
626                module.exit_json(changed=True, msg='Old stack instance(s) would be deleted', meta=[])
627        else:
628            # TODO: need to check the template and other settings for correct check mode
629            module.exit_json(changed=False, msg='No changes detected', meta=[])
630
631    changed = False
632    if state == 'present':
633        if not existing_stack_set:
634            # on create this parameter has a different name, and cannot be referenced later in the job log
635            stack_params['ClientRequestToken'] = 'Ansible-StackSet-Create-{0}'.format(operation_uuid)
636            changed = True
637            create_stack_set(module, stack_params, cfn)
638        else:
639            stack_params['OperationId'] = 'Ansible-StackSet-Update-{0}'.format(operation_uuid)
640            operation_ids.append(stack_params['OperationId'])
641            if module.params.get('regions'):
642                stack_params['OperationPreferences'] = get_operation_preferences(module)
643            changed |= update_stack_set(module, stack_params, cfn)
644
645        # now create/update any appropriate stack instances
646        new_stack_instances, existing_stack_instances, unspecified_stack_instances = compare_stack_instances(
647            cfn,
648            module.params['name'],
649            module.params['accounts'],
650            module.params['regions'],
651        )
652        if new_stack_instances:
653            operation_ids.append('Ansible-StackInstance-Create-{0}'.format(operation_uuid))
654            changed = True
655            cfn.create_stack_instances(
656                StackSetName=module.params['name'],
657                Accounts=list(set(acct for acct, region in new_stack_instances)),
658                Regions=list(set(region for acct, region in new_stack_instances)),
659                OperationPreferences=get_operation_preferences(module),
660                OperationId=operation_ids[-1],
661            )
662        else:
663            operation_ids.append('Ansible-StackInstance-Update-{0}'.format(operation_uuid))
664            cfn.update_stack_instances(
665                StackSetName=module.params['name'],
666                Accounts=list(set(acct for acct, region in existing_stack_instances)),
667                Regions=list(set(region for acct, region in existing_stack_instances)),
668                OperationPreferences=get_operation_preferences(module),
669                OperationId=operation_ids[-1],
670            )
671        for op in operation_ids:
672            await_stack_set_operation(
673                module, cfn, operation_id=op,
674                stack_set_name=module.params['name'],
675                max_wait=module.params.get('wait_timeout'),
676            )
677
678    elif state == 'absent':
679        if not existing_stack_set:
680            module.exit_json(msg='Stack set {0} does not exist'.format(module.params['name']))
681        if module.params.get('purge_stack_instances') is False:
682            pass
683        try:
684            cfn.delete_stack_set(
685                StackSetName=module.params['name'],
686            )
687            module.exit_json(msg='Stack set {0} deleted'.format(module.params['name']))
688        except is_boto3_error_code('OperationInProgressException') as e:  # pylint: disable=duplicate-except
689            module.fail_json_aws(e, msg='Cannot delete stack {0} while there is an operation in progress'.format(module.params['name']))
690        except is_boto3_error_code('StackSetNotEmptyException'):  # pylint: disable=duplicate-except
691            delete_instances_op = 'Ansible-StackInstance-Delete-{0}'.format(operation_uuid)
692            cfn.delete_stack_instances(
693                StackSetName=module.params['name'],
694                Accounts=module.params['accounts'],
695                Regions=module.params['regions'],
696                RetainStacks=(not module.params.get('purge_stacks')),
697                OperationId=delete_instances_op
698            )
699            await_stack_set_operation(
700                module, cfn, operation_id=delete_instances_op,
701                stack_set_name=stack_params['StackSetName'],
702                max_wait=module.params.get('wait_timeout'),
703            )
704            try:
705                cfn.delete_stack_set(
706                    StackSetName=module.params['name'],
707                )
708            except is_boto3_error_code('StackSetNotEmptyException') as exc:  # pylint: disable=duplicate-except
709                # this time, it is likely that either the delete failed or there are more stacks.
710                instances = cfn.list_stack_instances(
711                    StackSetName=module.params['name'],
712                )
713                stack_states = ', '.join('(account={Account}, region={Region}, state={Status})'.format(**i) for i in instances['Summaries'])
714                module.fail_json_aws(exc, msg='Could not purge all stacks, or not all accounts/regions were chosen for deletion: ' + stack_states)
715            module.exit_json(changed=True, msg='Stack set {0} deleted'.format(module.params['name']))
716
717    result.update(**describe_stack_tree(module, stack_params['StackSetName'], operation_ids=operation_ids))
718    if any(o['status'] == 'FAILED' for o in result['operations']):
719        module.fail_json(msg="One or more operations failed to execute", **result)
720    module.exit_json(changed=changed, **result)
721
722
723if __name__ == '__main__':
724    main()
725