def main(): argument_spec = dict( resource=dict(required=True), tags=dict(type='dict'), purge_tags=dict(type='bool', default=False), state=dict(default='present', choices=['present', 'absent', 'list']), ) required_if = [('state', 'present', ['tags']), ('state', 'absent', ['tags'])] module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) resource = module.params['resource'] tags = module.params['tags'] state = module.params['state'] purge_tags = module.params['purge_tags'] result = {'changed': False} jctanner.cloud_amazon.ec2 = module.client('jctanner.cloud_amazon.ec2') current_tags = get_tags(jctanner.cloud_amazon.ec2, module, resource) if state == 'list': module.exit_json(changed=False, tags=current_tags) add_tags, remove = compare_jctanner.cloud_amazon.aws_tags(current_tags, tags, purge_tags=purge_tags) remove_tags = {} if state == 'absent': for key in tags: if key in current_tags and (tags[key] is None or current_tags[key] == tags[key]): remove_tags[key] = current_tags[key] for key in remove: remove_tags[key] = current_tags[key] if remove_tags: result['changed'] = True result['removed_tags'] = remove_tags if not module.check_mode: try: jctanner.cloud_amazon.ec2.delete_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(remove_tags)) except (BotoCoreError, ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg='Failed to remove tags {0} from resource {1}'.format(remove_tags, resource)) if state == 'present' and add_tags: result['changed'] = True result['added_tags'] = add_tags current_tags.update(add_tags) if not module.check_mode: try: jctanner.cloud_amazon.ec2.create_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(add_tags)) except (BotoCoreError, ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg='Failed to set tags {0} on resource {1}'.format(add_tags, resource)) result['tags'] = get_tags(jctanner.cloud_amazon.ec2, module, resource) module.exit_json(**result)
def ensure_tags(self, tgw_id, tags, purge_tags): """ Ensures tags are applied to the transit gateway. Optionally will remove any existing tags not in the tags argument if purge_tags is set to true :param tgw_id: The AWS id of the transit gateway :param tags: list of tags to apply to the transit gateway. :param purge_tags: when true existing tags not in tags parms are removed :return: true if tags were updated """ tags_changed = False filters = ansible_dict_to_boto3_filter_list({'resource-id': tgw_id}) try: cur_tags = self._connection.describe_tags(Filters=filters) except (ClientError, BotoCoreError) as e: self._module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't describe tags") to_update, to_delete = compare_jctanner.cloud_amazon.aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) if to_update: try: if not self._check_mode: AWSRetry.exponential_backoff()(self._connection.create_tags)( Resources=[tgw_id], Tags=ansible_dict_to_boto3_tag_list(to_update) ) self._results['changed'] = True tags_changed = True except (ClientError, BotoCoreError) as e: self._module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't create tags {0} for resource {1}".format( ansible_dict_to_boto3_tag_list(to_update), tgw_id)) if to_delete: try: if not self._check_mode: tags_list = [] for key in to_delete: tags_list.append({'Key': key}) AWSRetry.exponential_backoff()(self._connection.delete_tags)( Resources=[tgw_id], Tags=tags_list ) self._results['changed'] = True tags_changed = True except (ClientError, BotoCoreError) as e: self._module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't delete tags {0} for resource {1}".format( ansible_dict_to_boto3_tag_list(to_delete), tgw_id)) return tags_changed
def ensure_tags(connection=None, module=None, resource_id=None, tags=None, purge_tags=None, check_mode=None): try: cur_tags = describe_tags_with_backoff(connection, resource_id) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg='Unable to list tags for VPC') to_add, to_delete = compare_jctanner.cloud_amazon.aws_tags(cur_tags, tags, purge_tags) if not to_add and not to_delete: return {'changed': False, 'tags': cur_tags} if check_mode: if not purge_tags: tags = cur_tags.update(tags) return {'changed': True, 'tags': tags} if to_delete: try: connection.delete_tags(Resources=[resource_id], Tags=[{'Key': k} for k in to_delete]) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't delete tags") if to_add: try: connection.create_tags(Resources=[resource_id], Tags=ansible_dict_to_boto3_tag_list(to_add)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't create tags") try: latest_tags = describe_tags_with_backoff(connection, resource_id) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg='Unable to list tags for VPC') return {'changed': True, 'tags': latest_tags}
def update_tags(module, connection, group, tags): changed = False existing_tags = connection.list_tags_for_resource(ResourceName=group['DBParameterGroupArn'])['TagList'] to_update, to_delete = compare_jctanner.cloud_amazon.aws_tags(boto3_tag_list_to_ansible_dict(existing_tags), tags, module.params['purge_tags']) if to_update: try: connection.add_tags_to_resource(ResourceName=group['DBParameterGroupArn'], Tags=ansible_dict_to_boto3_tag_list(to_update)) changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg="Couldn't add tags to parameter group: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) except botocore.exceptions.ParamValidationError as e: # Usually a tag value has been passed as an int or bool, needs to be a string # The AWS exception message is reasonably ok for this purpose module.fail_json(msg="Couldn't add tags to parameter group: %s." % str(e), exception=traceback.format_exc()) if to_delete: try: connection.remove_tags_from_resource(ResourceName=group['DBParameterGroupArn'], TagKeys=to_delete) changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg="Couldn't remove tags from parameter group: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) return changed
def create_key(connection, module): params = dict(BypassPolicyLockoutSafetyCheck=False, Tags=ansible_dict_to_boto3_tag_list(module.params['tags'], tag_name_key_name='TagKey', tag_value_key_name='TagValue'), KeyUsage='ENCRYPT_DECRYPT', Origin='AWS_KMS') if module.params.get('description'): params['Description'] = module.params['description'] if module.params.get('policy'): params['Policy'] = module.params['policy'] try: result = connection.create_key(**params)['KeyMetadata'] except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Failed to create initial key") key = get_key_details(connection, module, result['KeyId']) alias = module.params['alias'] if not alias.startswith('alias/'): alias = 'alias/' + alias try: connection.create_alias(AliasName=alias, TargetKeyId=key['key_id']) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Failed to create alias") ensure_enabled_disabled(connection, module, key) for grant in module.params.get('grants'): grant_params = convert_grant_params(grant, key) try: connection.create_grant(**grant_params) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Failed to add grant to key") # make results consistent with kms_facts result = get_key_details(connection, module, key['key_id']) module.exit_json(changed=True, **camel_dict_to_snake_dict(result))
def ensure_present(module, connection): groupname = module.params['name'] tags = module.params.get('tags') changed = False errors = [] try: response = connection.describe_db_parameter_groups(DBParameterGroupName=groupname) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == 'DBParameterGroupNotFound': response = None else: module.fail_json(msg="Couldn't access parameter group information: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) if not response: params = dict(DBParameterGroupName=groupname, DBParameterGroupFamily=module.params['engine'], Description=module.params['description']) if tags: params['Tags'] = ansible_dict_to_boto3_tag_list(tags) try: response = connection.create_db_parameter_group(**params) changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg="Couldn't create parameter group: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) else: group = response['DBParameterGroups'][0] if tags: changed = update_tags(module, connection, group, tags) if module.params.get('params'): params_changed, errors = update_parameters(module, connection) changed = changed or params_changed try: response = connection.describe_db_parameter_groups(DBParameterGroupName=groupname) group = camel_dict_to_snake_dict(response['DBParameterGroups'][0]) except botocore.exceptions.ClientError as e: module.fail_json(msg="Couldn't obtain parameter group information: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) try: tags = connection.list_tags_for_resource(ResourceName=group['db_parameter_group_arn'])['TagList'] except botocore.exceptions.ClientError as e: module.fail_json(msg="Couldn't obtain parameter group tags: %s" % str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) group['tags'] = boto3_tag_list_to_ansible_dict(tags) module.exit_json(changed=changed, errors=errors, **group)
def ensure_tags(conn, module, subnet, tags, purge_tags, start_time): changed = False filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'}) try: cur_tags = conn.describe_tags(Filters=filters) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't describe tags") to_update, to_delete = compare_jctanner.cloud_amazon.aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) if to_update: try: if not module.check_mode: AWSRetry.exponential_backoff( catch_extra_error_codes=['InvalidSubnetID.NotFound'] )(conn.create_tags)( Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_update) ) changed = True except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't create tags") if to_delete: try: if not module.check_mode: tags_list = [] for key in to_delete: tags_list.append({'Key': key}) AWSRetry.exponential_backoff( catch_extra_error_codes=['InvalidSubnetID.NotFound'] )(conn.delete_tags)(Resources=[subnet['id']], Tags=tags_list) changed = True except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Couldn't delete tags") if module.params['wait'] and not module.check_mode: # Wait for tags to be updated filters = [{'Name': 'tag:{0}'.format(k), 'Values': [v]} for k, v in tags.items()] handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) return changed
def update_image(module, connection, image_id): launch_permissions = module.params.get('launch_permissions') image = get_image_by_id(module, connection, image_id) if image is None: module.fail_json(msg="Image %s does not exist" % image_id, changed=False) changed = False if launch_permissions is not None: current_permissions = image['LaunchPermissions'] current_users = set(permission['UserId'] for permission in current_permissions if 'UserId' in permission) desired_users = set(str(user_id) for user_id in launch_permissions.get('user_ids', [])) current_groups = set(permission['Group'] for permission in current_permissions if 'Group' in permission) desired_groups = set(launch_permissions.get('group_names', [])) to_add_users = desired_users - current_users to_remove_users = current_users - desired_users to_add_groups = desired_groups - current_groups to_remove_groups = current_groups - desired_groups to_add = [dict(Group=group) for group in to_add_groups] + [dict(UserId=user_id) for user_id in to_add_users] to_remove = [dict(Group=group) for group in to_remove_groups] + [dict(UserId=user_id) for user_id in to_remove_users] if to_add or to_remove: try: connection.modify_image_attribute(ImageId=image_id, Attribute='launchPermission', LaunchPermission=dict(Add=to_add, Remove=to_remove)) changed = True except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error updating launch permissions of image %s" % image_id) desired_tags = module.params.get('tags') if desired_tags is not None: current_tags = boto3_tag_list_to_ansible_dict(image.get('Tags')) tags_to_add, tags_to_remove = compare_jctanner.cloud_amazon.aws_tags(current_tags, desired_tags, purge_tags=module.params.get('purge_tags')) if tags_to_remove: try: connection.delete_tags(Resources=[image_id], Tags=[dict(Key=tagkey) for tagkey in tags_to_remove]) changed = True except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error updating tags") if tags_to_add: try: connection.create_tags(Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags_to_add)) changed = True except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error updating tags") description = module.params.get('description') if description and description != image['Description']: try: connection.modify_image_attribute(Attribute='Description ', ImageId=image_id, Description=dict(Value=description)) changed = True except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error setting description for image %s" % image_id) if changed: module.exit_json(msg="AMI updated.", changed=True, **get_ami_info(get_image_by_id(module, connection, image_id))) else: module.exit_json(msg="AMI not updated.", changed=False, **get_ami_info(get_image_by_id(module, connection, image_id)))
def create_image(module, connection): instance_id = module.params.get('instance_id') name = module.params.get('name') wait = module.params.get('wait') wait_timeout = module.params.get('wait_timeout') description = module.params.get('description') architecture = module.params.get('architecture') kernel_id = module.params.get('kernel_id') root_device_name = module.params.get('root_device_name') virtualization_type = module.params.get('virtualization_type') no_reboot = module.params.get('no_reboot') device_mapping = module.params.get('device_mapping') tags = module.params.get('tags') launch_permissions = module.params.get('launch_permissions') image_location = module.params.get('image_location') enhanced_networking = module.params.get('enhanced_networking') billing_products = module.params.get('billing_products') ramdisk_id = module.params.get('ramdisk_id') sriov_net_support = module.params.get('sriov_net_support') try: params = { 'Name': name, 'Description': description } block_device_mapping = None if device_mapping: block_device_mapping = [] for device in device_mapping: device['Ebs'] = {} if 'device_name' not in device: module.fail_json(msg="Error - Device name must be set for volume.") device = rename_item_if_exists(device, 'device_name', 'DeviceName') device = rename_item_if_exists(device, 'virtual_name', 'VirtualName') device = rename_item_if_exists(device, 'no_device', 'NoDevice') device = rename_item_if_exists(device, 'volume_type', 'VolumeType', 'Ebs') device = rename_item_if_exists(device, 'snapshot_id', 'SnapshotId', 'Ebs') device = rename_item_if_exists(device, 'delete_on_termination', 'DeleteOnTermination', 'Ebs') device = rename_item_if_exists(device, 'size', 'VolumeSize', 'Ebs', attribute_type=int) device = rename_item_if_exists(device, 'volume_size', 'VolumeSize', 'Ebs', attribute_type=int) device = rename_item_if_exists(device, 'iops', 'Iops', 'Ebs') device = rename_item_if_exists(device, 'encrypted', 'Encrypted', 'Ebs') block_device_mapping.append(device) if block_device_mapping: params['BlockDeviceMappings'] = block_device_mapping if instance_id: params['InstanceId'] = instance_id params['NoReboot'] = no_reboot image_id = connection.create_image(**params).get('ImageId') else: if architecture: params['Architecture'] = architecture if virtualization_type: params['VirtualizationType'] = virtualization_type if image_location: params['ImageLocation'] = image_location if enhanced_networking: params['EnaSupport'] = enhanced_networking if billing_products: params['BillingProducts'] = billing_products if ramdisk_id: params['RamdiskId'] = ramdisk_id if sriov_net_support: params['SriovNetSupport'] = sriov_net_support if kernel_id: params['KernelId'] = kernel_id if root_device_name: params['RootDeviceName'] = root_device_name image_id = connection.register_image(**params).get('ImageId') except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error registering image") if wait: waiter = connection.get_waiter('image_available') delay = wait_timeout // 30 max_attempts = 30 waiter.wait(ImageIds=[image_id], WaiterConfig=dict(Delay=delay, MaxAttempts=max_attempts)) if tags: try: connection.create_tags(Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags)) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error tagging image") if launch_permissions: try: params = dict(Attribute='LaunchPermission', ImageId=image_id, LaunchPermission=dict(Add=list())) for group_name in launch_permissions.get('group_names', []): params['LaunchPermission']['Add'].append(dict(Group=group_name)) for user_id in launch_permissions.get('user_ids', []): params['LaunchPermission']['Add'].append(dict(UserId=str(user_id))) if params['LaunchPermission']['Add']: connection.modify_image_attribute(**params) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="Error setting launch permissions for image %s" % image_id) module.exit_json(msg="AMI creation operation complete.", changed=True, **get_ami_info(get_image_by_id(module, connection, image_id)))
def create_or_update_dynamo_table(connection, module, boto3_dynamodb=None, boto3_sts=None, region=None): table_name = module.params.get('name') hash_key_name = module.params.get('hash_key_name') hash_key_type = module.params.get('hash_key_type') range_key_name = module.params.get('range_key_name') range_key_type = module.params.get('range_key_type') read_capacity = module.params.get('read_capacity') write_capacity = module.params.get('write_capacity') all_indexes = module.params.get('indexes') tags = module.params.get('tags') wait_for_active_timeout = module.params.get('wait_for_active_timeout') for index in all_indexes: validate_index(index, module) schema = get_schema_param(hash_key_name, hash_key_type, range_key_name, range_key_type) throughput = { 'read': read_capacity, 'write': write_capacity } indexes, global_indexes = get_indexes(all_indexes) result = dict( region=region, table_name=table_name, hash_key_name=hash_key_name, hash_key_type=hash_key_type, range_key_name=range_key_name, range_key_type=range_key_type, read_capacity=read_capacity, write_capacity=write_capacity, indexes=all_indexes, ) try: table = Table(table_name, connection=connection) if dynamo_table_exists(table): result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode, global_indexes=global_indexes) else: if not module.check_mode: Table.create(table_name, connection=connection, schema=schema, throughput=throughput, indexes=indexes, global_indexes=global_indexes) result['changed'] = True if not module.check_mode: result['table_status'] = table.describe()['Table']['TableStatus'] if tags: # only tables which are active can be tagged wait_until_table_active(module, table, wait_for_active_timeout) account_id = get_account_id(boto3_sts) boto3_dynamodb.tag_resource( ResourceArn='arn:jctanner.cloud_amazon.aws:dynamodb:' + region + ':' + account_id + ':table/' + table_name, Tags=ansible_dict_to_boto3_tag_list(tags)) result['tags'] = tags except BotoServerError: result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc() module.fail_json(**result) else: module.exit_json(**result)
if module.params.get('kms_key_id'): params['KmsKeyId'] = module.params.get('kms_key_id') try: if module.params.get('tag_equality'): filters = [{'Name': 'tag:%s' % k, 'Values': [v]} for (k, v) in module.params.get('tags').items()] filters.append(dict(Name='state', Values=['available', 'pending'])) images = jctanner.cloud_amazon.ec2.describe_images(Filters=filters) if len(images['Images']) > 0: image = images['Images'][0] if not image: image = jctanner.cloud_amazon.ec2.copy_image(**params) image_id = image['ImageId'] if tags: jctanner.cloud_amazon.ec2.create_tags(Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags)) changed = True if module.params.get('wait'): delay = 15 max_attempts = module.params.get('wait_timeout') // delay image_id = image.get('ImageId') jctanner.cloud_amazon.ec2.get_waiter('image_available').wait( ImageIds=[image_id], WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts} ) module.exit_json(changed=changed, **camel_dict_to_snake_dict(image)) except WaiterError as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg='An error occurred waiting for the image to become available') except (ClientError, BotoCoreError) as e:
def main(): argument_spec = dict( name=dict(required=True), description=dict(), wait=dict(type='bool', default=False), wait_timeout=dict(type='int', default=900), state=dict(default='present', choices=['present', 'absent']), purge_stacks=dict(type='bool', default=True), parameters=dict(type='dict', default={}), template=dict(type='path'), template_url=dict(), template_body=dict(), capabilities=dict(type='list', choices=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']), regions=dict(type='list'), accounts=dict(type='list'), failure_tolerance=dict( type='dict', default={}, options=dict( fail_count=dict(type='int'), fail_percentage=dict(type='int'), parallel_percentage=dict(type='int'), parallel_count=dict(type='int'), ), mutually_exclusive=[ ['fail_count', 'fail_percentage'], ['parallel_count', 'parallel_percentage'], ], ), administration_role_arn=dict( aliases=['admin_role_arn', 'administration_role', 'admin_role']), execution_role_name=dict( aliases=['execution_role', 'exec_role', 'exec_role_name']), tags=dict(type='dict'), ) module = AnsibleAWSModule( argument_spec=argument_spec, mutually_exclusive=[['template_url', 'template', 'template_body']], supports_check_mode=True) if not (module.boto3_at_least('1.6.0') and module.botocore_at_least('1.10.26')): module.fail_json( msg= "Boto3 or botocore version is too low. This module requires at least boto3 1.6 and botocore 1.10.26" ) # Wrap the cloudformation client methods that this module uses with # automatic backoff / retry for throttling error codes cfn = module.client('cloudformation', retry_decorator=AWSRetry.jittered_backoff( retries=10, delay=3, max_delay=30)) existing_stack_set = stack_set_facts(cfn, module.params['name']) operation_uuid = to_native(uuid.uuid4()) operation_ids = [] # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around. stack_params = {} state = module.params['state'] if state == 'present' and not module.params['accounts']: module.fail_json( msg= "Can't create a stack set without choosing at least one account. " "To get the ID of the current account, use the jctanner.cloud_amazon.aws_caller_info module." ) module.params['accounts'] = [ to_native(a) for a in module.params['accounts'] ] stack_params['StackSetName'] = module.params['name'] if module.params.get('description'): stack_params['Description'] = module.params['description'] if module.params.get('capabilities'): stack_params['Capabilities'] = module.params['capabilities'] if module.params['template'] is not None: with open(module.params['template'], 'r') as tpl: stack_params['TemplateBody'] = tpl.read() elif module.params['template_body'] is not None: stack_params['TemplateBody'] = module.params['template_body'] elif module.params['template_url'] is not None: stack_params['TemplateURL'] = module.params['template_url'] else: # no template is provided, but if the stack set exists already, we can use the existing one. if existing_stack_set: stack_params['UsePreviousTemplate'] = True else: module.fail_json( msg= "The Stack Set {0} does not exist, and no template was provided. Provide one of `template`, " "`template_body`, or `template_url`".format( module.params['name'])) stack_params['Parameters'] = [] for k, v in module.params['parameters'].items(): if isinstance(v, dict): # set parameter based on a dict to allow additional CFN Parameter Attributes param = dict(ParameterKey=k) if 'value' in v: param['ParameterValue'] = to_native(v['value']) if 'use_previous_value' in v and bool(v['use_previous_value']): param['UsePreviousValue'] = True param.pop('ParameterValue', None) stack_params['Parameters'].append(param) else: # allow default k/v configuration to set a template parameter stack_params['Parameters'].append({ 'ParameterKey': k, 'ParameterValue': str(v) }) if module.params.get('tags') and isinstance(module.params.get('tags'), dict): stack_params['Tags'] = ansible_dict_to_boto3_tag_list( module.params['tags']) if module.params.get('administration_role_arn'): # TODO loosen the semantics here to autodetect the account ID and build the ARN stack_params['AdministrationRoleARN'] = module.params[ 'administration_role_arn'] if module.params.get('execution_role_name'): stack_params['ExecutionRoleName'] = module.params[ 'execution_role_name'] result = {} if module.check_mode: if state == 'absent' and existing_stack_set: module.exit_json(changed=True, msg='Stack set would be deleted', meta=[]) elif state == 'absent' and not existing_stack_set: module.exit_json(changed=False, msg='Stack set doesn\'t exist', meta=[]) elif state == 'present' and not existing_stack_set: module.exit_json(changed=True, msg='New stack set would be created', meta=[]) elif state == 'present' and existing_stack_set: new_stacks, existing_stacks, unspecified_stacks = compare_stack_instances( cfn, module.params['name'], module.params['accounts'], module.params['regions'], ) if new_stacks: module.exit_json(changed=True, msg='New stack instance(s) would be created', meta=[]) elif unspecified_stacks and module.params.get( 'purge_stack_instances'): module.exit_json(changed=True, msg='Old stack instance(s) would be deleted', meta=[]) else: # TODO: need to check the template and other settings for correct check mode module.exit_json(changed=False, msg='No changes detected', meta=[]) changed = False if state == 'present': if not existing_stack_set: # on create this parameter has a different name, and cannot be referenced later in the job log stack_params[ 'ClientRequestToken'] = 'Ansible-StackSet-Create-{0}'.format( operation_uuid) changed = True create_stack_set(module, stack_params, cfn) else: stack_params['OperationId'] = 'Ansible-StackSet-Update-{0}'.format( operation_uuid) operation_ids.append(stack_params['OperationId']) if module.params.get('regions'): stack_params[ 'OperationPreferences'] = get_operation_preferences(module) changed |= update_stack_set(module, stack_params, cfn) # now create/update any appropriate stack instances new_stack_instances, existing_stack_instances, unspecified_stack_instances = compare_stack_instances( cfn, module.params['name'], module.params['accounts'], module.params['regions'], ) if new_stack_instances: operation_ids.append( 'Ansible-StackInstance-Create-{0}'.format(operation_uuid)) changed = True cfn.create_stack_instances( StackSetName=module.params['name'], Accounts=list(set(acct for acct, region in new_stack_instances)), Regions=list( set(region for acct, region in new_stack_instances)), OperationPreferences=get_operation_preferences(module), OperationId=operation_ids[-1], ) else: operation_ids.append( 'Ansible-StackInstance-Update-{0}'.format(operation_uuid)) cfn.update_stack_instances( StackSetName=module.params['name'], Accounts=list( set(acct for acct, region in existing_stack_instances)), Regions=list( set(region for acct, region in existing_stack_instances)), OperationPreferences=get_operation_preferences(module), OperationId=operation_ids[-1], ) for op in operation_ids: await_stack_set_operation( module, cfn, operation_id=op, stack_set_name=module.params['name'], max_wait=module.params.get('wait_timeout'), ) elif state == 'absent': if not existing_stack_set: module.exit_json(msg='Stack set {0} does not exist'.format( module.params['name'])) if module.params.get('purge_stack_instances') is False: pass try: cfn.delete_stack_set(StackSetName=module.params['name'], ) module.exit_json( msg='Stack set {0} deleted'.format(module.params['name'])) except is_boto3_error_code('OperationInProgressException') as e: # pylint: disable=duplicate-except module.fail_json_jctanner.cloud_amazon.aws( e, msg= 'Cannot delete stack {0} while there is an operation in progress' .format(module.params['name'])) except is_boto3_error_code('StackSetNotEmptyException'): # pylint: disable=duplicate-except delete_instances_op = 'Ansible-StackInstance-Delete-{0}'.format( operation_uuid) cfn.delete_stack_instances( StackSetName=module.params['name'], Accounts=module.params['accounts'], Regions=module.params['regions'], RetainStacks=(not module.params.get('purge_stacks')), OperationId=delete_instances_op) await_stack_set_operation( module, cfn, operation_id=delete_instances_op, stack_set_name=stack_params['StackSetName'], max_wait=module.params.get('wait_timeout'), ) try: cfn.delete_stack_set(StackSetName=module.params['name'], ) except is_boto3_error_code('StackSetNotEmptyException') as exc: # pylint: disable=duplicate-except # this time, it is likely that either the delete failed or there are more stacks. instances = cfn.list_stack_instances( StackSetName=module.params['name'], ) stack_states = ', '.join( '(account={Account}, region={Region}, state={Status})'. format(**i) for i in instances['Summaries']) module.fail_json_jctanner.cloud_amazon.aws( exc, msg= 'Could not purge all stacks, or not all accounts/regions were chosen for deletion: ' + stack_states) module.exit_json(changed=True, msg='Stack set {0} deleted'.format( module.params['name'])) result.update(**describe_stack_tree( module, stack_params['StackSetName'], operation_ids=operation_ids)) if any(o['status'] == 'FAILED' for o in result['operations']): module.fail_json(msg="One or more operations failed to execute", **result) module.exit_json(changed=changed, **result)
def main(): argument_spec = dict( name=dict(required=True), state=dict(choices=['absent', 'present'], default='present'), tags=dict(type='dict'), ) required_if = [['state', 'present', ['tags']]] module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=False, required_if=required_if, ) if not HAS_BOTO3: module.fail_json(msg='boto3 and botocore are required for this module') name = module.params.get('name') state = module.params.get('state').lower() tags = module.params.get('tags') if tags: tags = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') client = module.client('inspector') try: existing_target_arn = client.list_assessment_targets( filter={'assessmentTargetNamePattern': name}, ).get('assessmentTargetArns')[0] existing_target = camel_dict_to_snake_dict( client.describe_assessment_targets( assessmentTargetArns=[existing_target_arn], ).get('assessmentTargets')[0] ) existing_resource_group_arn = existing_target.get('resource_group_arn') existing_resource_group_tags = client.describe_resource_groups( resourceGroupArns=[existing_resource_group_arn], ).get('resourceGroups')[0].get('tags') target_exists = True except ( botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError, ) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="trying to retrieve targets") except IndexError: target_exists = False if state == 'present' and target_exists: ansible_dict_tags = boto3_tag_list_to_ansible_dict(tags) ansible_dict_existing_tags = boto3_tag_list_to_ansible_dict( existing_resource_group_tags ) tags_to_add, tags_to_remove = compare_jctanner.cloud_amazon.aws_tags( ansible_dict_tags, ansible_dict_existing_tags ) if not (tags_to_add or tags_to_remove): existing_target.update({'tags': ansible_dict_existing_tags}) module.exit_json(changed=False, **existing_target) else: try: updated_resource_group_arn = client.create_resource_group( resourceGroupTags=tags, ).get('resourceGroupArn') client.update_assessment_target( assessmentTargetArn=existing_target_arn, assessmentTargetName=name, resourceGroupArn=updated_resource_group_arn, ) updated_target = camel_dict_to_snake_dict( client.describe_assessment_targets( assessmentTargetArns=[existing_target_arn], ).get('assessmentTargets')[0] ) updated_target.update({'tags': ansible_dict_tags}) module.exit_json(changed=True, **updated_target), except ( botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError, ) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="trying to update target") elif state == 'present' and not target_exists: try: new_resource_group_arn = client.create_resource_group( resourceGroupTags=tags, ).get('resourceGroupArn') new_target_arn = client.create_assessment_target( assessmentTargetName=name, resourceGroupArn=new_resource_group_arn, ).get('assessmentTargetArn') new_target = camel_dict_to_snake_dict( client.describe_assessment_targets( assessmentTargetArns=[new_target_arn], ).get('assessmentTargets')[0] ) new_target.update({'tags': boto3_tag_list_to_ansible_dict(tags)}) module.exit_json(changed=True, **new_target) except ( botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError, ) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="trying to create target") elif state == 'absent' and target_exists: try: client.delete_assessment_target( assessmentTargetArn=existing_target_arn, ) module.exit_json(changed=True) except ( botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError, ) as e: module.fail_json_jctanner.cloud_amazon.aws(e, msg="trying to delete target") elif state == 'absent' and not target_exists: module.exit_json(changed=False)