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