diff options
author | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:27 +0000 |
---|---|---|
committer | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:27 +0000 |
commit | cb06e04e718f73173a6dcae1b082637063bae786 (patch) | |
tree | deab34c907f665de2e033342ea8eec374f0775c8 /lib | |
parent | 5c9a9974c09c041a3bddca744af41fea7e51835f (diff) | |
download | ansible-cb06e04e718f73173a6dcae1b082637063bae786.tar.gz |
Migrated to community.amazon
Diffstat (limited to 'lib')
212 files changed, 0 insertions, 72947 deletions
diff --git a/lib/ansible/modules/cloud/amazon/_aws_acm_facts.py b/lib/ansible/modules/cloud/amazon/_aws_acm_facts.py deleted file mode 120000 index 42dbcf0df9..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_acm_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_acm_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_kms_facts.py b/lib/ansible/modules/cloud/amazon/_aws_kms_facts.py deleted file mode 120000 index ccd052f519..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_kms_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_kms_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_region_facts.py b/lib/ansible/modules/cloud/amazon/_aws_region_facts.py deleted file mode 120000 index 03b0d29932..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_region_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_region_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_s3_bucket_facts.py b/lib/ansible/modules/cloud/amazon/_aws_s3_bucket_facts.py deleted file mode 120000 index 88f68b437a..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_s3_bucket_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_s3_bucket_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_sgw_facts.py b/lib/ansible/modules/cloud/amazon/_aws_sgw_facts.py deleted file mode 120000 index 0af0560a3b..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_sgw_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_sgw_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_waf_facts.py b/lib/ansible/modules/cloud/amazon/_aws_waf_facts.py deleted file mode 120000 index 3fd538387a..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_waf_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_waf_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_cloudfront_facts.py b/lib/ansible/modules/cloud/amazon/_cloudfront_facts.py deleted file mode 120000 index 700056e714..0000000000 --- a/lib/ansible/modules/cloud/amazon/_cloudfront_facts.py +++ /dev/null @@ -1 +0,0 @@ -cloudfront_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_cloudwatchlogs_log_group_facts.py b/lib/ansible/modules/cloud/amazon/_cloudwatchlogs_log_group_facts.py deleted file mode 120000 index 402937478a..0000000000 --- a/lib/ansible/modules/cloud/amazon/_cloudwatchlogs_log_group_facts.py +++ /dev/null @@ -1 +0,0 @@ -cloudwatchlogs_log_group_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_asg_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_asg_facts.py deleted file mode 120000 index 88ec952458..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_asg_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_asg_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_customer_gateway_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_customer_gateway_facts.py deleted file mode 120000 index 2e1aec0aba..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_customer_gateway_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_customer_gateway_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_eip_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_eip_facts.py deleted file mode 120000 index 0ba519697b..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_eip_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_eip_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_elb_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_elb_facts.py deleted file mode 120000 index a029c6d0b0..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_elb_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_elb_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_instance_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_instance_facts.py deleted file mode 120000 index 7010fdcb95..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_instance_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_instance_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_lc_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_lc_facts.py deleted file mode 120000 index cb62597c07..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_lc_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_lc_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_placement_group_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_placement_group_facts.py deleted file mode 120000 index 7d33ef0167..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_placement_group_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_placement_group_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_endpoint_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_endpoint_facts.py deleted file mode 120000 index d2a144a7b8..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_endpoint_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_endpoint_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_igw_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_igw_facts.py deleted file mode 120000 index b3eeb3fee6..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_igw_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_igw_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_nacl_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_nacl_facts.py deleted file mode 120000 index a88962d88f..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_nacl_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_nacl_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_nat_gateway_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_nat_gateway_facts.py deleted file mode 120000 index fd96998997..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_nat_gateway_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_nat_gateway_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_peering_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_peering_facts.py deleted file mode 120000 index 074baf65a0..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_peering_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_peering_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_route_table_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_route_table_facts.py deleted file mode 120000 index ed0f72a1aa..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_route_table_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_route_table_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_vgw_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_vgw_facts.py deleted file mode 120000 index bbcf44bef4..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_vgw_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_vgw_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_vpn_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_vpn_facts.py deleted file mode 120000 index 671a1a3034..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_vpn_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_vpn_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ecs_service_facts.py b/lib/ansible/modules/cloud/amazon/_ecs_service_facts.py deleted file mode 120000 index fead2dab76..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ecs_service_facts.py +++ /dev/null @@ -1 +0,0 @@ -ecs_service_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ecs_taskdefinition_facts.py b/lib/ansible/modules/cloud/amazon/_ecs_taskdefinition_facts.py deleted file mode 120000 index 0eb6f10b8f..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ecs_taskdefinition_facts.py +++ /dev/null @@ -1 +0,0 @@ -ecs_taskdefinition_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_efs_facts.py b/lib/ansible/modules/cloud/amazon/_efs_facts.py deleted file mode 120000 index 781c362da4..0000000000 --- a/lib/ansible/modules/cloud/amazon/_efs_facts.py +++ /dev/null @@ -1 +0,0 @@ -efs_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_elasticache_facts.py b/lib/ansible/modules/cloud/amazon/_elasticache_facts.py deleted file mode 120000 index d6cd32eb0c..0000000000 --- a/lib/ansible/modules/cloud/amazon/_elasticache_facts.py +++ /dev/null @@ -1 +0,0 @@ -elasticache_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_elb_application_lb_facts.py b/lib/ansible/modules/cloud/amazon/_elb_application_lb_facts.py deleted file mode 120000 index c5ee0eaca8..0000000000 --- a/lib/ansible/modules/cloud/amazon/_elb_application_lb_facts.py +++ /dev/null @@ -1 +0,0 @@ -elb_application_lb_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_elb_classic_lb_facts.py b/lib/ansible/modules/cloud/amazon/_elb_classic_lb_facts.py deleted file mode 120000 index d182d5e144..0000000000 --- a/lib/ansible/modules/cloud/amazon/_elb_classic_lb_facts.py +++ /dev/null @@ -1 +0,0 @@ -elb_classic_lb_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_elb_target_facts.py b/lib/ansible/modules/cloud/amazon/_elb_target_facts.py deleted file mode 120000 index 897c23897d..0000000000 --- a/lib/ansible/modules/cloud/amazon/_elb_target_facts.py +++ /dev/null @@ -1 +0,0 @@ -elb_target_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_elb_target_group_facts.py b/lib/ansible/modules/cloud/amazon/_elb_target_group_facts.py deleted file mode 120000 index 3abd2ee5a6..0000000000 --- a/lib/ansible/modules/cloud/amazon/_elb_target_group_facts.py +++ /dev/null @@ -1 +0,0 @@ -elb_target_group_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_iam_cert_facts.py b/lib/ansible/modules/cloud/amazon/_iam_cert_facts.py deleted file mode 120000 index 63244caa58..0000000000 --- a/lib/ansible/modules/cloud/amazon/_iam_cert_facts.py +++ /dev/null @@ -1 +0,0 @@ -iam_server_certificate_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_iam_mfa_device_facts.py b/lib/ansible/modules/cloud/amazon/_iam_mfa_device_facts.py deleted file mode 120000 index 63be2b059f..0000000000 --- a/lib/ansible/modules/cloud/amazon/_iam_mfa_device_facts.py +++ /dev/null @@ -1 +0,0 @@ -iam_mfa_device_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_iam_role_facts.py b/lib/ansible/modules/cloud/amazon/_iam_role_facts.py deleted file mode 120000 index e15c454b71..0000000000 --- a/lib/ansible/modules/cloud/amazon/_iam_role_facts.py +++ /dev/null @@ -1 +0,0 @@ -iam_role_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_iam_server_certificate_facts.py b/lib/ansible/modules/cloud/amazon/_iam_server_certificate_facts.py deleted file mode 120000 index 63244caa58..0000000000 --- a/lib/ansible/modules/cloud/amazon/_iam_server_certificate_facts.py +++ /dev/null @@ -1 +0,0 @@ -iam_server_certificate_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_lambda_facts.py b/lib/ansible/modules/cloud/amazon/_lambda_facts.py deleted file mode 100644 index f332c2d9be..0000000000 --- a/lib/ansible/modules/cloud/amazon/_lambda_facts.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['deprecated'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: lambda_facts -deprecated: - removed_in: '2.13' - why: Deprecated in favour of C(_info) module. - alternative: Use M(lambda_info) instead. -short_description: Gathers AWS Lambda function details as Ansible facts -description: - - Gathers various details related to Lambda functions, including aliases, versions and event source mappings. - Use module M(lambda) to manage the lambda function itself, M(lambda_alias) to manage function aliases and - M(lambda_event) to manage lambda event source mappings. - -version_added: "2.2" - -options: - query: - description: - - Specifies the resource type for which to gather facts. Leave blank to retrieve all facts. - choices: [ "aliases", "all", "config", "mappings", "policy", "versions" ] - default: "all" - type: str - function_name: - description: - - The name of the lambda function for which facts are requested. - aliases: [ "function", "name"] - type: str - event_source_arn: - description: - - For query type 'mappings', this is the Amazon Resource Name (ARN) of the Amazon Kinesis or DynamoDB stream. - type: str -author: Pierre Jodouin (@pjodouin) -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -# Simple example of listing all info for a function -- name: List all for a specific function - lambda_facts: - query: all - function_name: myFunction - register: my_function_details -# List all versions of a function -- name: List function versions - lambda_facts: - query: versions - function_name: myFunction - register: my_function_versions -# List all lambda function versions -- name: List all function - lambda_facts: - query: all - max_items: 20 -- name: show Lambda facts - debug: - var: lambda_facts -''' - -RETURN = ''' ---- -lambda_facts: - description: lambda facts - returned: success - type: dict -lambda_facts.function: - description: lambda function list - returned: success - type: dict -lambda_facts.function.TheName: - description: lambda function information, including event, mapping, and version information - returned: success - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -import json -import datetime -import sys -import re - - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def fix_return(node): - """ - fixup returned dictionary - - :param node: - :return: - """ - - if isinstance(node, datetime.datetime): - node_value = str(node) - - elif isinstance(node, list): - node_value = [fix_return(item) for item in node] - - elif isinstance(node, dict): - node_value = dict([(item, fix_return(node[item])) for item in node.keys()]) - - else: - node_value = node - - return node_value - - -def alias_details(client, module): - """ - Returns list of aliases for a specified function. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_facts = dict() - - function_name = module.params.get('function_name') - if function_name: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - try: - lambda_facts.update(aliases=client.list_aliases(FunctionName=function_name, **params)['Aliases']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(aliases=[]) - else: - module.fail_json_aws(e, msg="Trying to get aliases") - else: - module.fail_json(msg='Parameter function_name required for query=aliases.') - - return {function_name: camel_dict_to_snake_dict(lambda_facts)} - - -def all_details(client, module): - """ - Returns all lambda related facts. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - if module.params.get('max_items') or module.params.get('next_marker'): - module.fail_json(msg='Cannot specify max_items nor next_marker for query=all.') - - lambda_facts = dict() - - function_name = module.params.get('function_name') - if function_name: - lambda_facts[function_name] = {} - lambda_facts[function_name].update(config_details(client, module)[function_name]) - lambda_facts[function_name].update(alias_details(client, module)[function_name]) - lambda_facts[function_name].update(policy_details(client, module)[function_name]) - lambda_facts[function_name].update(version_details(client, module)[function_name]) - lambda_facts[function_name].update(mapping_details(client, module)[function_name]) - else: - lambda_facts.update(config_details(client, module)) - - return lambda_facts - - -def config_details(client, module): - """ - Returns configuration details for one or all lambda functions. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_facts = dict() - - function_name = module.params.get('function_name') - if function_name: - try: - lambda_facts.update(client.get_function_configuration(FunctionName=function_name)) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(function={}) - else: - module.fail_json_aws(e, msg="Trying to get {0} configuration".format(function_name)) - else: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_facts.update(function_list=client.list_functions(**params)['Functions']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(function_list=[]) - else: - module.fail_json_aws(e, msg="Trying to get function list") - - functions = dict() - for func in lambda_facts.pop('function_list', []): - functions[func['FunctionName']] = camel_dict_to_snake_dict(func) - return functions - - return {function_name: camel_dict_to_snake_dict(lambda_facts)} - - -def mapping_details(client, module): - """ - Returns all lambda event source mappings. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_facts = dict() - params = dict() - function_name = module.params.get('function_name') - - if function_name: - params['FunctionName'] = module.params.get('function_name') - - if module.params.get('event_source_arn'): - params['EventSourceArn'] = module.params.get('event_source_arn') - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_facts.update(mappings=client.list_event_source_mappings(**params)['EventSourceMappings']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(mappings=[]) - else: - module.fail_json_aws(e, msg="Trying to get source event mappings") - - if function_name: - return {function_name: camel_dict_to_snake_dict(lambda_facts)} - - return camel_dict_to_snake_dict(lambda_facts) - - -def policy_details(client, module): - """ - Returns policy attached to a lambda function. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - if module.params.get('max_items') or module.params.get('next_marker'): - module.fail_json(msg='Cannot specify max_items nor next_marker for query=policy.') - - lambda_facts = dict() - - function_name = module.params.get('function_name') - if function_name: - try: - # get_policy returns a JSON string so must convert to dict before reassigning to its key - lambda_facts.update(policy=json.loads(client.get_policy(FunctionName=function_name)['Policy'])) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(policy={}) - else: - module.fail_json_aws(e, msg="Trying to get {0} policy".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=policy.') - - return {function_name: camel_dict_to_snake_dict(lambda_facts)} - - -def version_details(client, module): - """ - Returns all lambda function versions. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_facts = dict() - - function_name = module.params.get('function_name') - if function_name: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_facts.update(versions=client.list_versions_by_function(FunctionName=function_name, **params)['Versions']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_facts.update(versions=[]) - else: - module.fail_json_aws(e, msg="Trying to get {0} versions".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=versions.') - - return {function_name: camel_dict_to_snake_dict(lambda_facts)} - - -def main(): - """ - Main entry point. - - :return dict: ansible facts - """ - argument_spec = dict( - function_name=dict(required=False, default=None, aliases=['function', 'name']), - query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions'], default='all'), - event_source_arn=dict(required=False, default=None) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[], - required_together=[] - ) - - # validate function_name if present - function_name = module.params['function_name'] - if function_name: - if not re.search(r"^[\w\-:]+$", function_name): - module.fail_json( - msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) - ) - if len(function_name) > 64: - module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - - client = module.client('lambda') - - this_module = sys.modules[__name__] - - invocations = dict( - aliases='alias_details', - all='all_details', - config='config_details', - mappings='mapping_details', - policy='policy_details', - versions='version_details', - ) - - this_module_function = getattr(this_module, invocations[module.params['query']]) - all_facts = fix_return(this_module_function(client, module)) - - results = dict(ansible_facts={'lambda_facts': {'function': all_facts}}, changed=False) - - if module.check_mode: - results['msg'] = 'Check mode set but ignored for fact gathering only.' - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/_rds_instance_facts.py b/lib/ansible/modules/cloud/amazon/_rds_instance_facts.py deleted file mode 120000 index f3dda86727..0000000000 --- a/lib/ansible/modules/cloud/amazon/_rds_instance_facts.py +++ /dev/null @@ -1 +0,0 @@ -rds_instance_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_rds_snapshot_facts.py b/lib/ansible/modules/cloud/amazon/_rds_snapshot_facts.py deleted file mode 120000 index 7281d3b696..0000000000 --- a/lib/ansible/modules/cloud/amazon/_rds_snapshot_facts.py +++ /dev/null @@ -1 +0,0 @@ -rds_snapshot_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_redshift_facts.py b/lib/ansible/modules/cloud/amazon/_redshift_facts.py deleted file mode 120000 index 40a774faad..0000000000 --- a/lib/ansible/modules/cloud/amazon/_redshift_facts.py +++ /dev/null @@ -1 +0,0 @@ -redshift_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_route53_facts.py b/lib/ansible/modules/cloud/amazon/_route53_facts.py deleted file mode 120000 index 6b40f0529b..0000000000 --- a/lib/ansible/modules/cloud/amazon/_route53_facts.py +++ /dev/null @@ -1 +0,0 @@ -route53_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/aws_acm.py b/lib/ansible/modules/cloud/amazon/aws_acm.py deleted file mode 100644 index 9504b4c078..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_acm.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2019 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# This module is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software. If not, see <http://www.gnu.org/licenses/>. -# -# Author: -# - Matthew Davis <Matthew.Davis.2@team.telstra.com> -# on behalf of Telstra Corporation Limited - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_acm -short_description: Upload and delete certificates in the AWS Certificate Manager service -description: - - Import and delete certificates in Amazon Web Service's Certificate Manager (AWS ACM). - - > - This module does not currently interact with AWS-provided certificates. - It currently only manages certificates provided to AWS by the user. - - The ACM API allows users to upload multiple certificates for the same domain name, - and even multiple identical certificates. - This module attempts to restrict such freedoms, to be idempotent, as per the Ansible philosophy. - It does this through applying AWS resource "Name" tags to ACM certificates. - - > - When I(state=present), - if there is one certificate in ACM - with a C(Name) tag equal to the C(name_tag) parameter, - and an identical body and chain, - this task will succeed without effect. - - > - When I(state=present), - if there is one certificate in ACM - a I(Name) tag equal to the I(name_tag) parameter, - and a different body, - this task will overwrite that certificate. - - > - When I(state=present), - if there are multiple certificates in ACM - with a I(Name) tag equal to the I(name_tag) parameter, - this task will fail. - - > - When I(state=absent) and I(certificate_arn) is defined, - this module will delete the ACM resource with that ARN if it exists in this region, - and succeed without effect if it doesn't exist. - - > - When I(state=absent) and I(domain_name) is defined, - this module will delete all ACM resources in this AWS region with a corresponding domain name. - If there are none, it will succeed without effect. - - > - When I(state=absent) and I(certificate_arn) is not defined, - and I(domain_name) is not defined, - this module will delete all ACM resources in this AWS region with a corresponding I(Name) tag. - If there are none, it will succeed without effect. - - Note that this may not work properly with keys of size 4096 bits, due to a limitation of the ACM API. -version_added: "2.10" -options: - certificate: - description: - - The body of the PEM encoded public certificate. - - Required when I(state) is not C(absent). - - If your certificate is in a file, use C(lookup('file', 'path/to/cert.pem')). - type: str - - certificate_arn: - description: - - The ARN of a certificate in ACM to delete - - Ignored when I(state=present). - - If I(state=absent), you must provide one of I(certificate_arn), I(domain_name) or I(name_tag). - - > - If I(state=absent) and no resource exists with this ARN in this region, - the task will succeed with no effect. - - > - If I(state=absent) and the corresponding resource exists in a different region, - this task may report success without deleting that resource. - type: str - aliases: [arn] - - certificate_chain: - description: - - The body of the PEM encoded chain for your certificate. - - If your certificate chain is in a file, use C(lookup('file', 'path/to/chain.pem')). - - Ignored when I(state=absent) - type: str - - domain_name: - description: - - The domain name of the certificate. - - > - If I(state=absent) and I(domain_name) is specified, - this task will delete all ACM certificates with this domain. - - Exactly one of I(domain_name), I(name_tag) and I(certificate_arn) must be provided. - - > - If I(state=present) this must not be specified. - (Since the domain name is encoded within the public certificate's body.) - type: str - aliases: [domain] - - name_tag: - description: - - The unique identifier for tagging resources using AWS tags, with key I(Name). - - This can be any set of characters accepted by AWS for tag values. - - > - This is to ensure Ansible can treat certificates idempotently, - even though the ACM API allows duplicate certificates. - - If I(state=preset), this must be specified. - - > - If I(state=absent), you must provide exactly one of - I(certificate_arn), I(domain_name) or I(name_tag). - type: str - aliases: [name] - - private_key: - description: - - The body of the PEM encoded private key. - - Required when I(state=present). - - Ignored when I(state=absent). - - If your private key is in a file, use C(lookup('file', 'path/to/key.pem')). - type: str - - state: - description: - - > - If I(state=present), the specified public certificate and private key - will be uploaded, with I(Name) tag equal to I(name_tag). - - > - If I(state=absent), any certificates in this region - with a corresponding I(domain_name), I(name_tag) or I(certificate_arn) - will be deleted. - choices: [present, absent] - default: present - type: str -requirements: - - boto3 -author: - - Matthew Davis (@matt-telstra) on behalf of Telstra Corporation Limited -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' - -- name: upload a self-signed certificate - aws_acm: - certificate: "{{ lookup('file', 'cert.pem' ) }}" - privateKey: "{{ lookup('file', 'key.pem' ) }}" - name_tag: my_cert # to be applied through an AWS tag as "Name":"my_cert" - region: ap-southeast-2 # AWS region - -- name: create/update a certificate with a chain - aws_acm: - certificate: "{{ lookup('file', 'cert.pem' ) }}" - privateKey: "{{ lookup('file', 'key.pem' ) }}" - name_tag: my_cert - certificate_chain: "{{ lookup('file', 'chain.pem' ) }}" - state: present - region: ap-southeast-2 - register: cert_create - -- name: print ARN of cert we just created - debug: - var: cert_create.certificate.arn - -- name: delete the cert we just created - aws_acm: - name_tag: my_cert - state: absent - region: ap-southeast-2 - -- name: delete a certificate with a particular ARN - aws_acm: - certificate_arn: "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901" - state: absent - region: ap-southeast-2 - -- name: delete all certificates with a particular domain name - aws_acm: - domain_name: acm.ansible.com - state: absent - region: ap-southeast-2 - -''' - -RETURN = ''' -certificate: - description: Information about the certificate which was uploaded - type: complex - returned: when I(state=present) - contains: - arn: - description: The ARN of the certificate in ACM - type: str - returned: when I(state=present) - sample: "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901" - domain_name: - description: The domain name encoded within the public certificate - type: str - returned: when I(state=present) - sample: acm.ansible.com -arns: - description: A list of the ARNs of the certificates in ACM which were deleted - type: list - elements: str - returned: when I(state=absent) - sample: - - "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901" -''' - - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.acm import ACMServiceManager -from ansible.module_utils._text import to_text -import base64 -import re # regex library - - -# Takes in two text arguments -# Each a PEM encoded certificate -# Or a chain of PEM encoded certificates -# May include some lines between each chain in the cert, e.g. "Subject: ..." -# Returns True iff the chains/certs are functionally identical (including chain order) -def chain_compare(module, a, b): - - chain_a_pem = pem_chain_split(module, a) - chain_b_pem = pem_chain_split(module, b) - - if len(chain_a_pem) != len(chain_b_pem): - return False - - # Chain length is the same - for (ca, cb) in zip(chain_a_pem, chain_b_pem): - der_a = PEM_body_to_DER(module, ca) - der_b = PEM_body_to_DER(module, cb) - if der_a != der_b: - return False - - return True - - -# Takes in PEM encoded data with no headers -# returns equivilent DER as byte array -def PEM_body_to_DER(module, pem): - try: - der = base64.b64decode(to_text(pem)) - except (ValueError, TypeError) as e: - module.fail_json_aws(e, msg="Unable to decode certificate chain") - return der - - -# Store this globally to avoid repeated recompilation -pem_chain_split_regex = re.compile(r"------?BEGIN [A-Z0-9. ]*CERTIFICATE------?([a-zA-Z0-9\+\/=\s]+)------?END [A-Z0-9. ]*CERTIFICATE------?") - - -# Use regex to split up a chain or single cert into an array of base64 encoded data -# Using "-----BEGIN CERTIFICATE-----" and "----END CERTIFICATE----" -# Noting that some chains have non-pem data in between each cert -# This function returns only what's between the headers, excluding the headers -def pem_chain_split(module, pem): - - pem_arr = re.findall(pem_chain_split_regex, to_text(pem)) - - if len(pem_arr) == 0: - # This happens if the regex doesn't match at all - module.fail_json(msg="Unable to split certificate chain. Possibly zero-length chain?") - - return pem_arr - - -def main(): - argument_spec = dict( - certificate=dict(), - certificate_arn=dict(aliases=['arn']), - certificate_chain=dict(), - domain_name=dict(aliases=['domain']), - name_tag=dict(aliases=['name']), - private_key=dict(no_log=True), - state=dict(default='present', choices=['present', 'absent']) - ) - required_if = [ - ['state', 'present', ['certificate', 'name_tag', 'private_key']], - ] - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) - acm = ACMServiceManager(module) - - # Check argument requirements - if module.params['state'] == 'present': - if module.params['certificate_arn']: - module.fail_json(msg="Parameter 'certificate_arn' is only valid if parameter 'state' is specified as 'absent'") - else: # absent - # exactly one of these should be specified - absent_args = ['certificate_arn', 'domain_name', 'name_tag'] - if sum([(module.params[a] is not None) for a in absent_args]) != 1: - for a in absent_args: - module.debug("%s is %s" % (a, module.params[a])) - module.fail_json(msg="If 'state' is specified as 'absent' then exactly one of 'name_tag', certificate_arn' or 'domain_name' must be specified") - - if module.params['name_tag']: - tags = dict(Name=module.params['name_tag']) - else: - tags = None - - client = module.client('acm') - - # fetch the list of certificates currently in ACM - certificates = acm.get_certificates(client=client, - module=module, - domain_name=module.params['domain_name'], - arn=module.params['certificate_arn'], - only_tags=tags) - - module.debug("Found %d corresponding certificates in ACM" % len(certificates)) - - if module.params['state'] == 'present': - if len(certificates) > 1: - msg = "More than one certificate with Name=%s exists in ACM in this region" % module.params['name_tag'] - module.fail_json(msg=msg, certificates=certificates) - elif len(certificates) == 1: - # update the existing certificate - module.debug("Existing certificate found in ACM") - old_cert = certificates[0] # existing cert in ACM - if ('tags' not in old_cert) or ('Name' not in old_cert['tags']) or (old_cert['tags']['Name'] != module.params['name_tag']): - # shouldn't happen - module.fail_json(msg="Internal error, unsure which certificate to update", certificate=old_cert) - - if 'certificate' not in old_cert: - # shouldn't happen - module.fail_json(msg="Internal error, unsure what the existing cert in ACM is", certificate=old_cert) - - # Are the existing certificate in ACM and the local certificate the same? - same = True - same &= chain_compare(module, old_cert['certificate'], module.params['certificate']) - if module.params['certificate_chain']: - # Need to test this - # not sure if Amazon appends the cert itself to the chain when self-signed - same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate_chain']) - else: - # When there is no chain with a cert - # it seems Amazon returns the cert itself as the chain - same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate']) - - if same: - module.debug("Existing certificate in ACM is the same, doing nothing") - domain = acm.get_domain_of_cert(client=client, module=module, arn=old_cert['certificate_arn']) - module.exit_json(certificate=dict(domain_name=domain, arn=old_cert['certificate_arn']), changed=False) - else: - module.debug("Existing certificate in ACM is different, overwriting") - - # update cert in ACM - arn = acm.import_certificate(client, module, - certificate=module.params['certificate'], - private_key=module.params['private_key'], - certificate_chain=module.params['certificate_chain'], - arn=old_cert['certificate_arn'], - tags=tags) - domain = acm.get_domain_of_cert(client=client, module=module, arn=arn) - module.exit_json(certificate=dict(domain_name=domain, arn=arn), changed=True) - else: # len(certificates) == 0 - module.debug("No certificate in ACM. Creating new one.") - arn = acm.import_certificate(client=client, - module=module, - certificate=module.params['certificate'], - private_key=module.params['private_key'], - certificate_chain=module.params['certificate_chain'], - tags=tags) - domain = acm.get_domain_of_cert(client=client, module=module, arn=arn) - - module.exit_json(certificate=dict(domain_name=domain, arn=arn), changed=True) - - else: # state == absent - for cert in certificates: - acm.delete_certificate(client, module, cert['certificate_arn']) - module.exit_json(arns=[cert['certificate_arn'] for cert in certificates], - changed=(len(certificates) > 0)) - - -if __name__ == '__main__': - # tests() - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_acm_info.py b/lib/ansible/modules/cloud/amazon/aws_acm_info.py deleted file mode 100644 index 0687e13d38..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_acm_info.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_acm_info -short_description: Retrieve certificate information from AWS Certificate Manager service -description: - - Retrieve information for ACM certificates - - This module was called C(aws_acm_facts) before Ansible 2.9. The usage did not change. - - Note that this will not return information about uploaded keys of size 4096 bits, due to a limitation of the ACM API. -version_added: "2.5" -options: - certificate_arn: - description: - - If provided, the results will be filtered to show only the certificate with this ARN. - - If no certificate with this ARN exists, this task will fail. - - If a certificate with this ARN exists in a different region, this task will fail - aliases: - - arn - version_added: '2.10' - type: str - domain_name: - description: - - The domain name of an ACM certificate to limit the search to - aliases: - - name - type: str - statuses: - description: - - Status to filter the certificate results - choices: ['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT', 'REVOKED', 'FAILED'] - type: list - elements: str - tags: - description: - - Filter results to show only certificates with tags that match all the tags specified here. - type: dict - version_added: '2.10' -requirements: - - boto3 -author: - - Will Thames (@willthames) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: obtain all ACM certificates - aws_acm_info: - -- name: obtain all information for a single ACM certificate - aws_acm_info: - domain_name: "*.example_com" - -- name: obtain all certificates pending validation - aws_acm_info: - statuses: - - PENDING_VALIDATION - -- name: obtain all certificates with tag Name=foo and myTag=bar - aws_acm_info: - tags: - Name: foo - myTag: bar - - -# The output is still a list of certificates, just one item long. -- name: obtain information about a certificate with a particular ARN - aws_acm_info: - certificate_arn: "arn:aws:acm:ap-southeast-2:123456789876:certificate/abcdeabc-abcd-1234-4321-abcdeabcde12" - -''' - -RETURN = ''' -certificates: - description: A list of certificates - returned: always - type: complex - contains: - certificate: - description: The ACM Certificate body - returned: when certificate creation is complete - sample: '-----BEGIN CERTIFICATE-----\\nMII.....-----END CERTIFICATE-----\\n' - type: str - certificate_arn: - description: Certificate ARN - returned: always - sample: arn:aws:acm:ap-southeast-2:123456789012:certificate/abcd1234-abcd-1234-abcd-123456789abc - type: str - certificate_chain: - description: Full certificate chain for the certificate - returned: when certificate creation is complete - sample: '-----BEGIN CERTIFICATE-----\\nMII...\\n-----END CERTIFICATE-----\\n-----BEGIN CERTIFICATE-----\\n...' - type: str - created_at: - description: Date certificate was created - returned: always - sample: '2017-08-15T10:31:19+10:00' - type: str - domain_name: - description: Domain name for the certificate - returned: always - sample: '*.example.com' - type: str - domain_validation_options: - description: Options used by ACM to validate the certificate - returned: when certificate type is AMAZON_ISSUED - type: complex - contains: - domain_name: - description: Fully qualified domain name of the certificate - returned: always - sample: example.com - type: str - validation_domain: - description: The domain name ACM used to send validation emails - returned: always - sample: example.com - type: str - validation_emails: - description: A list of email addresses that ACM used to send domain validation emails - returned: always - sample: - - admin@example.com - - postmaster@example.com - type: list - elements: str - validation_status: - description: Validation status of the domain - returned: always - sample: SUCCESS - type: str - failure_reason: - description: Reason certificate request failed - returned: only when certificate issuing failed - type: str - sample: NO_AVAILABLE_CONTACTS - in_use_by: - description: A list of ARNs for the AWS resources that are using the certificate. - returned: always - sample: [] - type: list - elements: str - issued_at: - description: Date certificate was issued - returned: always - sample: '2017-01-01T00:00:00+10:00' - type: str - issuer: - description: Issuer of the certificate - returned: always - sample: Amazon - type: str - key_algorithm: - description: Algorithm used to generate the certificate - returned: always - sample: RSA-2048 - type: str - not_after: - description: Date after which the certificate is not valid - returned: always - sample: '2019-01-01T00:00:00+10:00' - type: str - not_before: - description: Date before which the certificate is not valid - returned: always - sample: '2017-01-01T00:00:00+10:00' - type: str - renewal_summary: - description: Information about managed renewal process - returned: when certificate is issued by Amazon and a renewal has been started - type: complex - contains: - domain_validation_options: - description: Options used by ACM to validate the certificate - returned: when certificate type is AMAZON_ISSUED - type: complex - contains: - domain_name: - description: Fully qualified domain name of the certificate - returned: always - sample: example.com - type: str - validation_domain: - description: The domain name ACM used to send validation emails - returned: always - sample: example.com - type: str - validation_emails: - description: A list of email addresses that ACM used to send domain validation emails - returned: always - sample: - - admin@example.com - - postmaster@example.com - type: list - elements: str - validation_status: - description: Validation status of the domain - returned: always - sample: SUCCESS - type: str - renewal_status: - description: Status of the domain renewal - returned: always - sample: PENDING_AUTO_RENEWAL - type: str - revocation_reason: - description: Reason for certificate revocation - returned: when the certificate has been revoked - sample: SUPERCEDED - type: str - revoked_at: - description: Date certificate was revoked - returned: when the certificate has been revoked - sample: '2017-09-01T10:00:00+10:00' - type: str - serial: - description: The serial number of the certificate - returned: always - sample: 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f - type: str - signature_algorithm: - description: Algorithm used to sign the certificate - returned: always - sample: SHA256WITHRSA - type: str - status: - description: Status of the certificate in ACM - returned: always - sample: ISSUED - type: str - subject: - description: The name of the entity that is associated with the public key contained in the certificate - returned: always - sample: CN=*.example.com - type: str - subject_alternative_names: - description: Subject Alternative Names for the certificate - returned: always - sample: - - '*.example.com' - type: list - elements: str - tags: - description: Tags associated with the certificate - returned: always - type: dict - sample: - Application: helloworld - Environment: test - type: - description: The source of the certificate - returned: always - sample: AMAZON_ISSUED - type: str -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.acm import ACMServiceManager - - -def main(): - argument_spec = dict( - certificate_arn=dict(aliases=['arn']), - domain_name=dict(aliases=['name']), - statuses=dict(type='list', choices=['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT', 'REVOKED', 'FAILED']), - tags=dict(type='dict'), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - acm_info = ACMServiceManager(module) - - if module._name == 'aws_acm_facts': - module.deprecate("The 'aws_acm_facts' module has been renamed to 'aws_acm_info'", version='2.13') - - client = module.client('acm') - - certificates = acm_info.get_certificates(client, module, - domain_name=module.params['domain_name'], - statuses=module.params['statuses'], - arn=module.params['certificate_arn'], - only_tags=module.params['tags']) - - if module.params['certificate_arn'] and len(certificates) != 1: - module.fail_json(msg="No certificate exists in this region with ARN %s" % module.params['certificate_arn']) - - module.exit_json(certificates=certificates) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_api_gateway.py b/lib/ansible/modules/cloud/amazon/aws_api_gateway.py deleted file mode 100644 index 769e5b45c9..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_api_gateway.py +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_api_gateway -short_description: Manage AWS API Gateway APIs -description: - - Allows for the management of API Gateway APIs - - Normally you should give the api_id since there is no other - stable guaranteed unique identifier for the API. If you do - not give api_id then a new API will be create each time - this is run. - - Beware that there are very hard limits on the rate that - you can call API Gateway's REST API. You may need to patch - your boto. See U(https://github.com/boto/boto3/issues/876) - and discuss with your AWS rep. - - swagger_file and swagger_text are passed directly on to AWS - transparently whilst swagger_dict is an ansible dict which is - converted to JSON before the API definitions are uploaded. -version_added: '2.4' -requirements: [ boto3 ] -options: - api_id: - description: - - The ID of the API you want to manage. - type: str - state: - description: Create or delete API Gateway. - default: present - choices: [ 'present', 'absent' ] - type: str - swagger_file: - description: - - JSON or YAML file containing swagger definitions for API. - Exactly one of swagger_file, swagger_text or swagger_dict must - be present. - type: path - aliases: ['src', 'api_file'] - swagger_text: - description: - - Swagger definitions for API in JSON or YAML as a string direct - from playbook. - type: str - swagger_dict: - description: - - Swagger definitions API ansible dictionary which will be - converted to JSON and uploaded. - type: json - stage: - description: - - The name of the stage the API should be deployed to. - type: str - deploy_desc: - description: - - Description of the deployment - recorded and visible in the - AWS console. - default: Automatic deployment by Ansible. - type: str - cache_enabled: - description: - - Enable API GW caching of backend responses. Defaults to false. - type: bool - default: false - version_added: '2.10' - cache_size: - description: - - Size in GB of the API GW cache, becomes effective when cache_enabled is true. - choices: ['0.5', '1.6', '6.1', '13.5', '28.4', '58.2', '118', '237'] - type: str - default: '0.5' - version_added: '2.10' - stage_variables: - description: - - ENV variables for the stage. Define a dict of key values pairs for variables. - type: dict - version_added: '2.10' - stage_canary_settings: - description: - - Canary settings for the deployment of the stage. - - 'Dict with following settings:' - - 'percentTraffic: The percent (0-100) of traffic diverted to a canary deployment.' - - 'deploymentId: The ID of the canary deployment.' - - 'stageVariableOverrides: Stage variables overridden for a canary release deployment.' - - 'useStageCache: A Boolean flag to indicate whether the canary deployment uses the stage cache or not.' - - See docs U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/apigateway.html#APIGateway.Client.create_stage) - type: dict - version_added: '2.10' - tracing_enabled: - description: - - Specifies whether active tracing with X-ray is enabled for the API GW stage. - type: bool - version_added: '2.10' - endpoint_type: - description: - - Type of endpoint configuration, use C(EDGE) for an edge optimized API endpoint, - - C(REGIONAL) for just a regional deploy or PRIVATE for a private API. - - This will flag will only be used when creating a new API Gateway setup, not for updates. - choices: ['EDGE', 'REGIONAL', 'PRIVATE'] - type: str - default: EDGE - version_added: '2.10' -author: - - 'Michael De La Rue (@mikedlr)' -extends_documentation_fragment: - - aws - - ec2 -notes: - - A future version of this module will probably use tags or another - ID so that an API can be create only once. - - As an early work around an intermediate version will probably do - the same using a tag embedded in the API name. - -''' - -EXAMPLES = ''' -- name: Setup AWS API Gateway setup on AWS and deploy API definition - aws_api_gateway: - swagger_file: my_api.yml - stage: production - cache_enabled: true - cache_size: '1.6' - tracing_enabled: true - endpoint_type: EDGE - state: present - -- name: Update API definition to deploy new version - aws_api_gateway: - api_id: 'abc123321cba' - swagger_file: my_api.yml - deploy_desc: Make auth fix available. - cache_enabled: true - cache_size: '1.6' - endpoint_type: EDGE - state: present - -- name: Update API definitions and settings and deploy as canary - aws_api_gateway: - api_id: 'abc123321cba' - swagger_file: my_api.yml - cache_enabled: true - cache_size: '6.1' - canary_settings: { percentTraffic: 50.0, deploymentId: '123', useStageCache: True } - state: present -''' - -RETURN = ''' -api_id: - description: API id of the API endpoint created - returned: success - type: str - sample: '0ln4zq7p86' -configure_response: - description: AWS response from the API configure call - returned: success - type: dict - sample: { api_key_source: "HEADER", created_at: "2020-01-01T11:37:59+00:00", id: "0ln4zq7p86" } -deploy_response: - description: AWS response from the API deploy call - returned: success - type: dict - sample: { created_date: "2020-01-01T11:36:59+00:00", id: "rptv4b", description: "Automatic deployment by Ansible." } -resource_actions: - description: Actions performed against AWS API - returned: always - type: list - sample: ["apigateway:CreateRestApi", "apigateway:CreateDeployment", "apigateway:PutRestApi"] -''' - -import json - -try: - import botocore -except ImportError: - # HAS_BOTOCORE taken care of in AnsibleAWSModule - pass - -import traceback -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (AWSRetry, camel_dict_to_snake_dict) - - -def main(): - argument_spec = dict( - api_id=dict(type='str', required=False), - state=dict(type='str', default='present', choices=['present', 'absent']), - swagger_file=dict(type='path', default=None, aliases=['src', 'api_file']), - swagger_dict=dict(type='json', default=None), - swagger_text=dict(type='str', default=None), - stage=dict(type='str', default=None), - deploy_desc=dict(type='str', default="Automatic deployment by Ansible."), - cache_enabled=dict(type='bool', default=False), - cache_size=dict(type='str', default='0.5', choices=['0.5', '1.6', '6.1', '13.5', '28.4', '58.2', '118', '237']), - stage_variables=dict(type='dict', default={}), - stage_canary_settings=dict(type='dict', default={}), - tracing_enabled=dict(type='bool', default=False), - endpoint_type=dict(type='str', default='EDGE', choices=['EDGE', 'REGIONAL', 'PRIVATE']) - ) - - mutually_exclusive = [['swagger_file', 'swagger_dict', 'swagger_text']] # noqa: F841 - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=False, - mutually_exclusive=mutually_exclusive, - ) - - api_id = module.params.get('api_id') - state = module.params.get('state') # noqa: F841 - swagger_file = module.params.get('swagger_file') - swagger_dict = module.params.get('swagger_dict') - swagger_text = module.params.get('swagger_text') - endpoint_type = module.params.get('endpoint_type') - - client = module.client('apigateway') - - changed = True # for now it will stay that way until we can sometimes avoid change - conf_res = None - dep_res = None - del_res = None - - if state == "present": - if api_id is None: - api_id = create_empty_api(module, client, endpoint_type) - api_data = get_api_definitions(module, swagger_file=swagger_file, - swagger_dict=swagger_dict, swagger_text=swagger_text) - conf_res, dep_res = ensure_api_in_correct_state(module, client, api_id, api_data) - if state == "absent": - del_res = delete_rest_api(module, client, api_id) - - exit_args = {"changed": changed, "api_id": api_id} - - if conf_res is not None: - exit_args['configure_response'] = camel_dict_to_snake_dict(conf_res) - if dep_res is not None: - exit_args['deploy_response'] = camel_dict_to_snake_dict(dep_res) - if del_res is not None: - exit_args['delete_response'] = camel_dict_to_snake_dict(del_res) - - module.exit_json(**exit_args) - - -def get_api_definitions(module, swagger_file=None, swagger_dict=None, swagger_text=None): - apidata = None - if swagger_file is not None: - try: - with open(swagger_file) as f: - apidata = f.read() - except OSError as e: - msg = "Failed trying to read swagger file {0}: {1}".format(str(swagger_file), str(e)) - module.fail_json(msg=msg, exception=traceback.format_exc()) - if swagger_dict is not None: - apidata = json.dumps(swagger_dict) - if swagger_text is not None: - apidata = swagger_text - - if apidata is None: - module.fail_json(msg='module error - no swagger info provided') - return apidata - - -def create_empty_api(module, client, endpoint_type): - """ - creates a new empty API ready to be configured. The description is - temporarily set to show the API as incomplete but should be - updated when the API is configured. - """ - desc = "Incomplete API creation by ansible aws_api_gateway module" - try: - awsret = create_api(client, name="ansible-temp-api", description=desc, endpoint_type=endpoint_type) - except (botocore.exceptions.ClientError, botocore.exceptions.EndpointConnectionError) as e: - module.fail_json_aws(e, msg="creating API") - return awsret["id"] - - -def delete_rest_api(module, client, api_id): - """ - Deletes entire REST API setup - """ - try: - delete_response = delete_api(client, api_id) - except (botocore.exceptions.ClientError, botocore.exceptions.EndpointConnectionError) as e: - module.fail_json_aws(e, msg="deleting API {0}".format(api_id)) - return delete_response - - -def ensure_api_in_correct_state(module, client, api_id, api_data): - """Make sure that we have the API configured and deployed as instructed. - - This function first configures the API correctly uploading the - swagger definitions and then deploys those. Configuration and - deployment should be closely tied because there is only one set of - definitions so if we stop, they may be updated by someone else and - then we deploy the wrong configuration. - """ - - configure_response = None - try: - configure_response = configure_api(client, api_id, api_data=api_data) - except (botocore.exceptions.ClientError, botocore.exceptions.EndpointConnectionError) as e: - module.fail_json_aws(e, msg="configuring API {0}".format(api_id)) - - deploy_response = None - - stage = module.params.get('stage') - if stage: - try: - deploy_response = create_deployment(client, api_id, **module.params) - except (botocore.exceptions.ClientError, botocore.exceptions.EndpointConnectionError) as e: - msg = "deploying api {0} to stage {1}".format(api_id, stage) - module.fail_json_aws(e, msg) - - return configure_response, deploy_response - - -retry_params = {"tries": 10, "delay": 5, "backoff": 1.2} - - -@AWSRetry.backoff(**retry_params) -def create_api(client, name=None, description=None, endpoint_type=None): - return client.create_rest_api(name="ansible-temp-api", description=description, endpointConfiguration={'types': [endpoint_type]}) - - -@AWSRetry.backoff(**retry_params) -def delete_api(client, api_id): - return client.delete_rest_api(restApiId=api_id) - - -@AWSRetry.backoff(**retry_params) -def configure_api(client, api_id, api_data=None, mode="overwrite"): - return client.put_rest_api(restApiId=api_id, mode=mode, body=api_data) - - -@AWSRetry.backoff(**retry_params) -def create_deployment(client, rest_api_id, **params): - canary_settings = params.get('stage_canary_settings') - - if canary_settings and len(canary_settings) > 0: - result = client.create_deployment( - restApiId=rest_api_id, - stageName=params.get('stage'), - description=params.get('deploy_desc'), - cacheClusterEnabled=params.get('cache_enabled'), - cacheClusterSize=params.get('cache_size'), - variables=params.get('stage_variables'), - canarySettings=canary_settings, - tracingEnabled=params.get('tracing_enabled') - ) - else: - result = client.create_deployment( - restApiId=rest_api_id, - stageName=params.get('stage'), - description=params.get('deploy_desc'), - cacheClusterEnabled=params.get('cache_enabled'), - cacheClusterSize=params.get('cache_size'), - variables=params.get('stage_variables'), - tracingEnabled=params.get('tracing_enabled') - ) - - return result - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_application_scaling_policy.py b/lib/ansible/modules/cloud/amazon/aws_application_scaling_policy.py deleted file mode 100644 index 6a3fca9a1e..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_application_scaling_policy.py +++ /dev/null @@ -1,543 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_application_scaling_policy -short_description: Manage Application Auto Scaling Scaling Policies -notes: - - for details of the parameters and returns see - U(http://boto3.readthedocs.io/en/latest/reference/services/application-autoscaling.html#ApplicationAutoScaling.Client.put_scaling_policy) -description: - - Creates, updates or removes a Scaling Policy -version_added: "2.5" -author: - - Gustavo Maia (@gurumaia) - - Chen Leibovich (@chenl87) -requirements: [ json, botocore, boto3 ] -options: - state: - description: Whether a policy should be present or absent - required: yes - choices: ['absent', 'present'] - type: str - policy_name: - description: The name of the scaling policy. - required: yes - type: str - service_namespace: - description: The namespace of the AWS service. - required: yes - choices: ['ecs', 'elasticmapreduce', 'ec2', 'appstream', 'dynamodb'] - type: str - resource_id: - description: The identifier of the resource associated with the scalable target. - required: yes - type: str - scalable_dimension: - description: The scalable dimension associated with the scalable target. - required: yes - choices: [ 'ecs:service:DesiredCount', - 'ec2:spot-fleet-request:TargetCapacity', - 'elasticmapreduce:instancegroup:InstanceCount', - 'appstream:fleet:DesiredCapacity', - 'dynamodb:table:ReadCapacityUnits', - 'dynamodb:table:WriteCapacityUnits', - 'dynamodb:index:ReadCapacityUnits', - 'dynamodb:index:WriteCapacityUnits'] - type: str - policy_type: - description: The policy type. - required: yes - choices: ['StepScaling', 'TargetTrackingScaling'] - type: str - step_scaling_policy_configuration: - description: A step scaling policy. This parameter is required if you are creating a policy and the policy type is StepScaling. - required: no - type: dict - target_tracking_scaling_policy_configuration: - description: - - A target tracking policy. This parameter is required if you are creating a new policy and the policy type is TargetTrackingScaling. - - 'Full documentation of the suboptions can be found in the API documentation:' - - 'U(https://docs.aws.amazon.com/autoscaling/application/APIReference/API_TargetTrackingScalingPolicyConfiguration.html)' - required: no - type: dict - suboptions: - CustomizedMetricSpecification: - description: The metric to use if using a customized metric. - type: dict - DisableScaleIn: - description: Whether scaling-in should be disabled. - type: bool - PredefinedMetricSpecification: - description: The metric to use if using a predefined metric. - type: dict - ScaleInCooldown: - description: The time (in seconds) to wait after scaling-in before another scaling action can occur. - type: int - ScaleOutCooldown: - description: The time (in seconds) to wait after scaling-out before another scaling action can occur. - type: int - TargetValue: - description: The target value for the metric - type: float - minimum_tasks: - description: The minimum value to scale to in response to a scale in event. - This parameter is required if you are creating a first new policy for the specified service. - required: no - version_added: "2.6" - type: int - maximum_tasks: - description: The maximum value to scale to in response to a scale out event. - This parameter is required if you are creating a first new policy for the specified service. - required: no - version_added: "2.6" - type: int - override_task_capacity: - description: Whether or not to override values of minimum and/or maximum tasks if it's already set. - required: no - default: no - type: bool - version_added: "2.6" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create step scaling policy for ECS Service -- name: scaling_policy - aws_application_scaling_policy: - state: present - policy_name: test_policy - service_namespace: ecs - resource_id: service/poc-pricing/test-as - scalable_dimension: ecs:service:DesiredCount - policy_type: StepScaling - minimum_tasks: 1 - maximum_tasks: 6 - step_scaling_policy_configuration: - AdjustmentType: ChangeInCapacity - StepAdjustments: - - MetricIntervalUpperBound: 123 - ScalingAdjustment: 2 - - MetricIntervalLowerBound: 123 - ScalingAdjustment: -2 - Cooldown: 123 - MetricAggregationType: Average - -# Create target tracking scaling policy for ECS Service -- name: scaling_policy - aws_application_scaling_policy: - state: present - policy_name: test_policy - service_namespace: ecs - resource_id: service/poc-pricing/test-as - scalable_dimension: ecs:service:DesiredCount - policy_type: TargetTrackingScaling - minimum_tasks: 1 - maximum_tasks: 6 - target_tracking_scaling_policy_configuration: - TargetValue: 60 - PredefinedMetricSpecification: - PredefinedMetricType: ECSServiceAverageCPUUtilization - ScaleOutCooldown: 60 - ScaleInCooldown: 60 - -# Remove scalable target for ECS Service -- name: scaling_policy - aws_application_scaling_policy: - state: absent - policy_name: test_policy - policy_type: StepScaling - service_namespace: ecs - resource_id: service/cluster-name/service-name - scalable_dimension: ecs:service:DesiredCount -''' - -RETURN = ''' -alarms: - description: List of the CloudWatch alarms associated with the scaling policy - returned: when state present - type: complex - contains: - alarm_arn: - description: The Amazon Resource Name (ARN) of the alarm - returned: when state present - type: str - alarm_name: - description: The name of the alarm - returned: when state present - type: str -service_namespace: - description: The namespace of the AWS service. - returned: when state present - type: str - sample: ecs -resource_id: - description: The identifier of the resource associated with the scalable target. - returned: when state present - type: str - sample: service/cluster-name/service-name -scalable_dimension: - description: The scalable dimension associated with the scalable target. - returned: when state present - type: str - sample: ecs:service:DesiredCount -policy_arn: - description: The Amazon Resource Name (ARN) of the scaling policy.. - returned: when state present - type: str -policy_name: - description: The name of the scaling policy. - returned: when state present - type: str -policy_type: - description: The policy type. - returned: when state present - type: str -min_capacity: - description: The minimum value to scale to in response to a scale in event. Required if I(state) is C(present). - returned: when state present - type: int - sample: 1 -max_capacity: - description: The maximum value to scale to in response to a scale out event. Required if I(state) is C(present). - returned: when state present - type: int - sample: 2 -role_arn: - description: The ARN of an IAM role that allows Application Auto Scaling to modify the scalable target on your behalf. Required if I(state) is C(present). - returned: when state present - type: str - sample: arn:aws:iam::123456789123:role/roleName -step_scaling_policy_configuration: - description: The step scaling policy. - returned: when state present and the policy type is StepScaling - type: complex - contains: - adjustment_type: - description: The adjustment type - returned: when state present and the policy type is StepScaling - type: str - sample: "ChangeInCapacity, PercentChangeInCapacity, ExactCapacity" - cooldown: - description: The amount of time, in seconds, after a scaling activity completes - where previous trigger-related scaling activities can influence future scaling events - returned: when state present and the policy type is StepScaling - type: int - sample: 60 - metric_aggregation_type: - description: The aggregation type for the CloudWatch metrics - returned: when state present and the policy type is StepScaling - type: str - sample: "Average, Minimum, Maximum" - step_adjustments: - description: A set of adjustments that enable you to scale based on the size of the alarm breach - returned: when state present and the policy type is StepScaling - type: list - elements: dict -target_tracking_scaling_policy_configuration: - description: The target tracking policy. - returned: when state present and the policy type is TargetTrackingScaling - type: complex - contains: - predefined_metric_specification: - description: A predefined metric - returned: when state present and the policy type is TargetTrackingScaling - type: complex - contains: - predefined_metric_type: - description: The metric type - returned: when state present and the policy type is TargetTrackingScaling - type: str - sample: "ECSServiceAverageCPUUtilization, ECSServiceAverageMemoryUtilization" - resource_label: - description: Identifies the resource associated with the metric type - returned: when metric type is ALBRequestCountPerTarget - type: str - scale_in_cooldown: - description: The amount of time, in seconds, after a scale in activity completes before another scale in activity can start - returned: when state present and the policy type is TargetTrackingScaling - type: int - sample: 60 - scale_out_cooldown: - description: The amount of time, in seconds, after a scale out activity completes before another scale out activity can start - returned: when state present and the policy type is TargetTrackingScaling - type: int - sample: 60 - target_value: - description: The target value for the metric - returned: when state present and the policy type is TargetTrackingScaling - type: int - sample: 70 -creation_time: - description: The Unix timestamp for when the scalable target was created. - returned: when state present - type: str - sample: '2017-09-28T08:22:51.881000-03:00' -''' # NOQA - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import _camel_to_snake, camel_dict_to_snake_dict - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - - -# Merge the results of the scalable target creation and policy deletion/creation -# There's no risk in overriding values since mutual keys have the same values in our case -def merge_results(scalable_target_result, policy_result): - if scalable_target_result['changed'] or policy_result['changed']: - changed = True - else: - changed = False - - merged_response = scalable_target_result['response'].copy() - merged_response.update(policy_result['response']) - - return {"changed": changed, "response": merged_response} - - -def delete_scaling_policy(connection, module): - changed = False - try: - scaling_policy = connection.describe_scaling_policies( - ServiceNamespace=module.params.get('service_namespace'), - ResourceId=module.params.get('resource_id'), - ScalableDimension=module.params.get('scalable_dimension'), - PolicyNames=[module.params.get('policy_name')], - MaxResults=1 - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe scaling policies") - - if scaling_policy['ScalingPolicies']: - try: - connection.delete_scaling_policy( - ServiceNamespace=module.params.get('service_namespace'), - ResourceId=module.params.get('resource_id'), - ScalableDimension=module.params.get('scalable_dimension'), - PolicyName=module.params.get('policy_name'), - ) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to delete scaling policy") - - return {"changed": changed} - - -def create_scalable_target(connection, module): - changed = False - - try: - scalable_targets = connection.describe_scalable_targets( - ServiceNamespace=module.params.get('service_namespace'), - ResourceIds=[ - module.params.get('resource_id'), - ], - ScalableDimension=module.params.get('scalable_dimension') - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe scalable targets") - - # Scalable target registration will occur if: - # 1. There is no scalable target registered for this service - # 2. A scalable target exists, different min/max values are defined and override is set to "yes" - if ( - not scalable_targets['ScalableTargets'] - or ( - module.params.get('override_task_capacity') - and ( - scalable_targets['ScalableTargets'][0]['MinCapacity'] != module.params.get('minimum_tasks') - or scalable_targets['ScalableTargets'][0]['MaxCapacity'] != module.params.get('maximum_tasks') - ) - ) - ): - changed = True - try: - connection.register_scalable_target( - ServiceNamespace=module.params.get('service_namespace'), - ResourceId=module.params.get('resource_id'), - ScalableDimension=module.params.get('scalable_dimension'), - MinCapacity=module.params.get('minimum_tasks'), - MaxCapacity=module.params.get('maximum_tasks') - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to register scalable target") - - try: - response = connection.describe_scalable_targets( - ServiceNamespace=module.params.get('service_namespace'), - ResourceIds=[ - module.params.get('resource_id'), - ], - ScalableDimension=module.params.get('scalable_dimension') - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe scalable targets") - - if (response['ScalableTargets']): - snaked_response = camel_dict_to_snake_dict(response['ScalableTargets'][0]) - else: - snaked_response = {} - - return {"changed": changed, "response": snaked_response} - - -def create_scaling_policy(connection, module): - try: - scaling_policy = connection.describe_scaling_policies( - ServiceNamespace=module.params.get('service_namespace'), - ResourceId=module.params.get('resource_id'), - ScalableDimension=module.params.get('scalable_dimension'), - PolicyNames=[module.params.get('policy_name')], - MaxResults=1 - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe scaling policies") - - changed = False - - if scaling_policy['ScalingPolicies']: - scaling_policy = scaling_policy['ScalingPolicies'][0] - # check if the input parameters are equal to what's already configured - for attr in ('PolicyName', - 'ServiceNamespace', - 'ResourceId', - 'ScalableDimension', - 'PolicyType', - 'StepScalingPolicyConfiguration', - 'TargetTrackingScalingPolicyConfiguration'): - if attr in scaling_policy and scaling_policy[attr] != module.params.get(_camel_to_snake(attr)): - changed = True - scaling_policy[attr] = module.params.get(_camel_to_snake(attr)) - else: - changed = True - scaling_policy = { - 'PolicyName': module.params.get('policy_name'), - 'ServiceNamespace': module.params.get('service_namespace'), - 'ResourceId': module.params.get('resource_id'), - 'ScalableDimension': module.params.get('scalable_dimension'), - 'PolicyType': module.params.get('policy_type'), - 'StepScalingPolicyConfiguration': module.params.get('step_scaling_policy_configuration'), - 'TargetTrackingScalingPolicyConfiguration': module.params.get('target_tracking_scaling_policy_configuration') - } - - if changed: - try: - if (module.params.get('step_scaling_policy_configuration')): - connection.put_scaling_policy( - PolicyName=scaling_policy['PolicyName'], - ServiceNamespace=scaling_policy['ServiceNamespace'], - ResourceId=scaling_policy['ResourceId'], - ScalableDimension=scaling_policy['ScalableDimension'], - PolicyType=scaling_policy['PolicyType'], - StepScalingPolicyConfiguration=scaling_policy['StepScalingPolicyConfiguration'] - ) - elif (module.params.get('target_tracking_scaling_policy_configuration')): - connection.put_scaling_policy( - PolicyName=scaling_policy['PolicyName'], - ServiceNamespace=scaling_policy['ServiceNamespace'], - ResourceId=scaling_policy['ResourceId'], - ScalableDimension=scaling_policy['ScalableDimension'], - PolicyType=scaling_policy['PolicyType'], - TargetTrackingScalingPolicyConfiguration=scaling_policy['TargetTrackingScalingPolicyConfiguration'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to create scaling policy") - - try: - response = connection.describe_scaling_policies( - ServiceNamespace=module.params.get('service_namespace'), - ResourceId=module.params.get('resource_id'), - ScalableDimension=module.params.get('scalable_dimension'), - PolicyNames=[module.params.get('policy_name')], - MaxResults=1 - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe scaling policies") - - if (response['ScalingPolicies']): - snaked_response = camel_dict_to_snake_dict(response['ScalingPolicies'][0]) - else: - snaked_response = {} - - return {"changed": changed, "response": snaked_response} - - -def main(): - argument_spec = dict( - state=dict(type='str', required=True, choices=['present', 'absent']), - policy_name=dict(type='str', required=True), - service_namespace=dict(type='str', required=True, choices=['appstream', 'dynamodb', 'ec2', 'ecs', 'elasticmapreduce']), - resource_id=dict(type='str', required=True), - scalable_dimension=dict(type='str', - required=True, - choices=['ecs:service:DesiredCount', - 'ec2:spot-fleet-request:TargetCapacity', - 'elasticmapreduce:instancegroup:InstanceCount', - 'appstream:fleet:DesiredCapacity', - 'dynamodb:table:ReadCapacityUnits', - 'dynamodb:table:WriteCapacityUnits', - 'dynamodb:index:ReadCapacityUnits', - 'dynamodb:index:WriteCapacityUnits']), - policy_type=dict(type='str', required=True, choices=['StepScaling', 'TargetTrackingScaling']), - step_scaling_policy_configuration=dict(type='dict'), - target_tracking_scaling_policy_configuration=dict( - type='dict', - options=dict( - CustomizedMetricSpecification=dict(type='dict'), - DisableScaleIn=dict(type='bool'), - PredefinedMetricSpecification=dict(type='dict'), - ScaleInCooldown=dict(type='int'), - ScaleOutCooldown=dict(type='int'), - TargetValue=dict(type='float'), - ) - ), - minimum_tasks=dict(type='int'), - maximum_tasks=dict(type='int'), - override_task_capacity=dict(type='bool'), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - connection = module.client('application-autoscaling') - - # Remove any target_tracking_scaling_policy_configuration suboptions that are None - policy_config_options = [ - 'CustomizedMetricSpecification', 'DisableScaleIn', 'PredefinedMetricSpecification', 'ScaleInCooldown', 'ScaleOutCooldown', 'TargetValue' - ] - if isinstance(module.params['target_tracking_scaling_policy_configuration'], dict): - for option in policy_config_options: - if module.params['target_tracking_scaling_policy_configuration'][option] is None: - module.params['target_tracking_scaling_policy_configuration'].pop(option) - - if module.params.get("state") == 'present': - # A scalable target must be registered prior to creating a scaling policy - scalable_target_result = create_scalable_target(connection, module) - policy_result = create_scaling_policy(connection, module) - # Merge the results of the scalable target creation and policy deletion/creation - # There's no risk in overriding values since mutual keys have the same values in our case - merged_result = merge_results(scalable_target_result, policy_result) - module.exit_json(**merged_result) - else: - policy_result = delete_scaling_policy(connection, module) - module.exit_json(**policy_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_batch_compute_environment.py b/lib/ansible/modules/cloud/amazon/aws_batch_compute_environment.py deleted file mode 100644 index aee7b5c058..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_batch_compute_environment.py +++ /dev/null @@ -1,490 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Jon Meran <jonathan.meran@sonos.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_batch_compute_environment -short_description: Manage AWS Batch Compute Environments -description: - - This module allows the management of AWS Batch Compute Environments. - It is idempotent and supports "Check" mode. Use module M(aws_batch_compute_environment) to manage the compute - environment, M(aws_batch_job_queue) to manage job queues, M(aws_batch_job_definition) to manage job definitions. - -version_added: "2.5" - -author: Jon Meran (@jonmer85) -options: - compute_environment_name: - description: - - The name for your compute environment. Up to 128 letters (uppercase and lowercase), numbers, and underscores - are allowed. - required: true - type: str - type: - description: - - The type of the compute environment. - required: true - choices: ["MANAGED", "UNMANAGED"] - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - compute_environment_state: - description: - - The state of the compute environment. If the state is ENABLED, then the compute environment accepts jobs - from a queue and can scale out automatically based on queues. - default: "ENABLED" - choices: ["ENABLED", "DISABLED"] - type: str - service_role: - description: - - The full Amazon Resource Name (ARN) of the IAM role that allows AWS Batch to make calls to other AWS - services on your behalf. - required: true - type: str - compute_resource_type: - description: - - The type of compute resource. - required: true - choices: ["EC2", "SPOT"] - type: str - minv_cpus: - description: - - The minimum number of EC2 vCPUs that an environment should maintain. - required: true - type: int - maxv_cpus: - description: - - The maximum number of EC2 vCPUs that an environment can reach. - required: true - type: int - desiredv_cpus: - description: - - The desired number of EC2 vCPUS in the compute environment. - type: int - instance_types: - description: - - The instance types that may be launched. - required: true - type: list - elements: str - image_id: - description: - - The Amazon Machine Image (AMI) ID used for instances launched in the compute environment. - type: str - subnets: - description: - - The VPC subnets into which the compute resources are launched. - required: true - type: list - elements: str - security_group_ids: - description: - - The EC2 security groups that are associated with instances launched in the compute environment. - required: true - type: list - elements: str - ec2_key_pair: - description: - - The EC2 key pair that is used for instances launched in the compute environment. - type: str - instance_role: - description: - - The Amazon ECS instance role applied to Amazon EC2 instances in a compute environment. - required: true - type: str - tags: - description: - - Key-value pair tags to be applied to resources that are launched in the compute environment. - type: dict - bid_percentage: - description: - - The minimum percentage that a Spot Instance price must be when compared with the On-Demand price for that - instance type before instances are launched. For example, if your bid percentage is 20%, then the Spot price - must be below 20% of the current On-Demand price for that EC2 instance. - type: int - spot_iam_fleet_role: - description: - - The Amazon Resource Name (ARN) of the Amazon EC2 Spot Fleet IAM role applied to a SPOT compute environment. - type: str - -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -- hosts: localhost - gather_facts: no - vars: - state: present - tasks: - - name: My Batch Compute Environment - aws_batch_compute_environment: - compute_environment_name: computeEnvironmentName - state: present - region: us-east-1 - compute_environment_state: ENABLED - type: MANAGED - compute_resource_type: EC2 - minv_cpus: 0 - maxv_cpus: 2 - desiredv_cpus: 1 - instance_types: - - optimal - subnets: - - my-subnet1 - - my-subnet2 - security_group_ids: - - my-sg1 - - my-sg2 - instance_role: arn:aws:iam::<account>:instance-profile/<role> - tags: - tag1: value1 - tag2: value2 - service_role: arn:aws:iam::<account>:role/service-role/<role> - register: aws_batch_compute_environment_action - - - name: show results - debug: - var: aws_batch_compute_environment_action -''' - -RETURN = ''' ---- -output: - description: "returns what action was taken, whether something was changed, invocation and response" - returned: always - sample: - batch_compute_environment_action: none - changed: false - invocation: - module_args: - aws_access_key: ~ - aws_secret_key: ~ - bid_percentage: ~ - compute_environment_name: <name> - compute_environment_state: ENABLED - compute_resource_type: EC2 - desiredv_cpus: 0 - ec2_key_pair: ~ - ec2_url: ~ - image_id: ~ - instance_role: "arn:aws:iam::..." - instance_types: - - optimal - maxv_cpus: 8 - minv_cpus: 0 - profile: ~ - region: us-east-1 - security_group_ids: - - "*******" - security_token: ~ - service_role: "arn:aws:iam::...." - spot_iam_fleet_role: ~ - state: present - subnets: - - "******" - tags: - Environment: <name> - Name: <name> - type: MANAGED - validate_certs: true - response: - computeEnvironmentArn: "arn:aws:batch:...." - computeEnvironmentName: <name> - computeResources: - desiredvCpus: 0 - instanceRole: "arn:aws:iam::..." - instanceTypes: - - optimal - maxvCpus: 8 - minvCpus: 0 - securityGroupIds: - - "******" - subnets: - - "*******" - tags: - Environment: <name> - Name: <name> - type: EC2 - ecsClusterArn: "arn:aws:ecs:....." - serviceRole: "arn:aws:iam::..." - state: ENABLED - status: VALID - statusReason: "ComputeEnvironment Healthy" - type: MANAGED - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import snake_dict_to_camel_dict, camel_dict_to_snake_dict -import re - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - - -# --------------------------------------------------------------------------------------------------- -# -# Helper Functions & classes -# -# --------------------------------------------------------------------------------------------------- - -def set_api_params(module, module_params): - """ - Sets module parameters to those expected by the boto3 API. - - :param module: - :param module_params: - :return: - """ - api_params = dict((k, v) for k, v in dict(module.params).items() if k in module_params and v is not None) - return snake_dict_to_camel_dict(api_params) - - -def validate_params(module): - """ - Performs basic parameter validation. - - :param module: - :return: - """ - - compute_environment_name = module.params['compute_environment_name'] - - # validate compute environment name - if not re.search(r'^[\w\_:]+$', compute_environment_name): - module.fail_json( - msg="Function compute_environment_name {0} is invalid. Names must contain only alphanumeric characters " - "and underscores.".format(compute_environment_name) - ) - if not compute_environment_name.startswith('arn:aws:batch:'): - if len(compute_environment_name) > 128: - module.fail_json(msg='compute_environment_name "{0}" exceeds 128 character limit' - .format(compute_environment_name)) - - return - - -# --------------------------------------------------------------------------------------------------- -# -# Batch Compute Environment functions -# -# --------------------------------------------------------------------------------------------------- - -def get_current_compute_environment(module, client): - try: - environments = client.describe_compute_environments( - computeEnvironments=[module.params['compute_environment_name']] - ) - if len(environments['computeEnvironments']) > 0: - return environments['computeEnvironments'][0] - else: - return None - except ClientError: - return None - - -def create_compute_environment(module, client): - """ - Adds a Batch compute environment - - :param module: - :param client: - :return: - """ - - changed = False - - # set API parameters - params = ( - 'compute_environment_name', 'type', 'service_role') - api_params = set_api_params(module, params) - - if module.params['compute_environment_state'] is not None: - api_params['state'] = module.params['compute_environment_state'] - - compute_resources_param_list = ('minv_cpus', 'maxv_cpus', 'desiredv_cpus', 'instance_types', 'image_id', 'subnets', - 'security_group_ids', 'ec2_key_pair', 'instance_role', 'tags', 'bid_percentage', - 'spot_iam_fleet_role') - compute_resources_params = set_api_params(module, compute_resources_param_list) - - if module.params['compute_resource_type'] is not None: - compute_resources_params['type'] = module.params['compute_resource_type'] - - # if module.params['minv_cpus'] is not None: - # compute_resources_params['minvCpus'] = module.params['minv_cpus'] - - api_params['computeResources'] = compute_resources_params - - try: - if not module.check_mode: - client.create_compute_environment(**api_params) - changed = True - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg='Error creating compute environment') - - return changed - - -def remove_compute_environment(module, client): - """ - Remove a Batch compute environment - - :param module: - :param client: - :return: - """ - - changed = False - - # set API parameters - api_params = {'computeEnvironment': module.params['compute_environment_name']} - - try: - if not module.check_mode: - client.delete_compute_environment(**api_params) - changed = True - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg='Error removing compute environment') - return changed - - -def manage_state(module, client): - changed = False - current_state = 'absent' - state = module.params['state'] - compute_environment_state = module.params['compute_environment_state'] - compute_environment_name = module.params['compute_environment_name'] - service_role = module.params['service_role'] - minv_cpus = module.params['minv_cpus'] - maxv_cpus = module.params['maxv_cpus'] - desiredv_cpus = module.params['desiredv_cpus'] - action_taken = 'none' - update_env_response = '' - - check_mode = module.check_mode - - # check if the compute environment exists - current_compute_environment = get_current_compute_environment(module, client) - response = current_compute_environment - if current_compute_environment: - current_state = 'present' - - if state == 'present': - if current_state == 'present': - updates = False - # Update Batch Compute Environment configuration - compute_kwargs = {'computeEnvironment': compute_environment_name} - - # Update configuration if needed - compute_resources = {} - if compute_environment_state and current_compute_environment['state'] != compute_environment_state: - compute_kwargs.update({'state': compute_environment_state}) - updates = True - if service_role and current_compute_environment['serviceRole'] != service_role: - compute_kwargs.update({'serviceRole': service_role}) - updates = True - if minv_cpus is not None and current_compute_environment['computeResources']['minvCpus'] != minv_cpus: - compute_resources['minvCpus'] = minv_cpus - if maxv_cpus is not None and current_compute_environment['computeResources']['maxvCpus'] != maxv_cpus: - compute_resources['maxvCpus'] = maxv_cpus - if desiredv_cpus is not None and current_compute_environment['computeResources']['desiredvCpus'] != desiredv_cpus: - compute_resources['desiredvCpus'] = desiredv_cpus - if len(compute_resources) > 0: - compute_kwargs['computeResources'] = compute_resources - updates = True - if updates: - try: - if not check_mode: - update_env_response = client.update_compute_environment(**compute_kwargs) - if not update_env_response: - module.fail_json(msg='Unable to get compute environment information after creating') - changed = True - action_taken = "updated" - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update environment.") - - else: - # Create Batch Compute Environment - changed = create_compute_environment(module, client) - # Describe compute environment - action_taken = 'added' - response = get_current_compute_environment(module, client) - if not response: - module.fail_json(msg='Unable to get compute environment information after creating') - else: - if current_state == 'present': - # remove the compute environment - changed = remove_compute_environment(module, client) - action_taken = 'deleted' - return dict(changed=changed, batch_compute_environment_action=action_taken, response=response) - - -# --------------------------------------------------------------------------------------------------- -# -# MAIN -# -# --------------------------------------------------------------------------------------------------- - -def main(): - """ - Main entry point. - - :return dict: changed, batch_compute_environment_action, response - """ - - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent']), - compute_environment_name=dict(required=True), - type=dict(required=True, choices=['MANAGED', 'UNMANAGED']), - compute_environment_state=dict(required=False, default='ENABLED', choices=['ENABLED', 'DISABLED']), - service_role=dict(required=True), - compute_resource_type=dict(required=True, choices=['EC2', 'SPOT']), - minv_cpus=dict(type='int', required=True), - maxv_cpus=dict(type='int', required=True), - desiredv_cpus=dict(type='int'), - instance_types=dict(type='list', required=True), - image_id=dict(), - subnets=dict(type='list', required=True), - security_group_ids=dict(type='list', required=True), - ec2_key_pair=dict(), - instance_role=dict(required=True), - tags=dict(type='dict'), - bid_percentage=dict(type='int'), - spot_iam_fleet_role=dict(), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - client = module.client('batch') - - validate_params(module) - - results = manage_state(module, client) - - module.exit_json(**camel_dict_to_snake_dict(results, ignore_list=['Tags'])) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_batch_job_definition.py b/lib/ansible/modules/cloud/amazon/aws_batch_job_definition.py deleted file mode 100644 index 959677c42d..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_batch_job_definition.py +++ /dev/null @@ -1,459 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Jon Meran <jonathan.meran@sonos.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_batch_job_definition -short_description: Manage AWS Batch Job Definitions -description: - - This module allows the management of AWS Batch Job Definitions. - It is idempotent and supports "Check" mode. Use module M(aws_batch_compute_environment) to manage the compute - environment, M(aws_batch_job_queue) to manage job queues, M(aws_batch_job_definition) to manage job definitions. - -version_added: "2.5" - -author: Jon Meran (@jonmer85) -options: - job_definition_arn: - description: - - The ARN for the job definition. - type: str - job_definition_name: - description: - - The name for the job definition. - required: true - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - type: - description: - - The type of job definition. - required: true - type: str - parameters: - description: - - Default parameter substitution placeholders to set in the job definition. Parameters are specified as a - key-value pair mapping. Parameters in a SubmitJob request override any corresponding parameter defaults from - the job definition. - type: dict - image: - description: - - The image used to start a container. This string is passed directly to the Docker daemon. Images in the Docker - Hub registry are available by default. Other repositories are specified with `` repository-url /image <colon>tag ``. - Up to 255 letters (uppercase and lowercase), numbers, hyphens, underscores, colons, periods, forward slashes, - and number signs are allowed. This parameter maps to Image in the Create a container section of the Docker - Remote API and the IMAGE parameter of docker run. - required: true - type: str - vcpus: - description: - - The number of vCPUs reserved for the container. This parameter maps to CpuShares in the Create a container - section of the Docker Remote API and the --cpu-shares option to docker run. Each vCPU is equivalent to - 1,024 CPU shares. - required: true - type: int - memory: - description: - - The hard limit (in MiB) of memory to present to the container. If your container attempts to exceed the memory - specified here, the container is killed. This parameter maps to Memory in the Create a container section of the - Docker Remote API and the --memory option to docker run. - required: true - type: int - command: - description: - - The command that is passed to the container. This parameter maps to Cmd in the Create a container section of - the Docker Remote API and the COMMAND parameter to docker run. For more information, - see U(https://docs.docker.com/engine/reference/builder/#cmd). - type: list - elements: str - job_role_arn: - description: - - The Amazon Resource Name (ARN) of the IAM role that the container can assume for AWS permissions. - type: str - volumes: - description: - - A list of data volumes used in a job. - suboptions: - host: - description: - - The contents of the host parameter determine whether your data volume persists on the host container - instance and where it is stored. If the host parameter is empty, then the Docker daemon assigns a host - path for your data volume, but the data is not guaranteed to persist after the containers associated with - it stop running. - This is a dictionary with one property, sourcePath - The path on the host container - instance that is presented to the container. If this parameter is empty,then the Docker daemon has assigned - a host path for you. If the host parameter contains a sourcePath file location, then the data volume - persists at the specified location on the host container instance until you delete it manually. If the - sourcePath value does not exist on the host container instance, the Docker daemon creates it. If the - location does exist, the contents of the source path folder are exported. - name: - description: - - The name of the volume. Up to 255 letters (uppercase and lowercase), numbers, hyphens, and underscores are - allowed. This name is referenced in the sourceVolume parameter of container definition mountPoints. - type: list - elements: dict - environment: - description: - - The environment variables to pass to a container. This parameter maps to Env in the Create a container section - of the Docker Remote API and the --env option to docker run. - suboptions: - name: - description: - - The name of the key value pair. For environment variables, this is the name of the environment variable. - value: - description: - - The value of the key value pair. For environment variables, this is the value of the environment variable. - type: list - elements: dict - mount_points: - description: - - The mount points for data volumes in your container. This parameter maps to Volumes in the Create a container - section of the Docker Remote API and the --volume option to docker run. - suboptions: - containerPath: - description: - - The path on the container at which to mount the host volume. - readOnly: - description: - - If this value is true , the container has read-only access to the volume; otherwise, the container can write - to the volume. The default value is C(false). - sourceVolume: - description: - - The name of the volume to mount. - type: list - elements: dict - readonly_root_filesystem: - description: - - When this parameter is true, the container is given read-only access to its root file system. This parameter - maps to ReadonlyRootfs in the Create a container section of the Docker Remote API and the --read-only option - to docker run. - type: str - privileged: - description: - - When this parameter is true, the container is given elevated privileges on the host container instance - (similar to the root user). This parameter maps to Privileged in the Create a container section of the - Docker Remote API and the --privileged option to docker run. - type: str - ulimits: - description: - - A list of ulimits to set in the container. This parameter maps to Ulimits in the Create a container section - of the Docker Remote API and the --ulimit option to docker run. - suboptions: - hardLimit: - description: - - The hard limit for the ulimit type. - name: - description: - - The type of the ulimit. - softLimit: - description: - - The soft limit for the ulimit type. - type: list - elements: dict - user: - description: - - The user name to use inside the container. This parameter maps to User in the Create a container section of - the Docker Remote API and the --user option to docker run. - type: str - attempts: - description: - - Retry strategy - The number of times to move a job to the RUNNABLE status. You may specify between 1 and 10 - attempts. If attempts is greater than one, the job is retried if it fails until it has moved to RUNNABLE that - many times. - type: int -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -- hosts: localhost - gather_facts: no - vars: - state: present - tasks: -- name: My Batch Job Definition - aws_batch_job_definition: - job_definition_name: My Batch Job Definition - state: present - type: container - parameters: - Param1: Val1 - Param2: Val2 - image: <Docker Image URL> - vcpus: 1 - memory: 512 - command: - - python - - run_my_script.py - - arg1 - job_role_arn: <Job Role ARN> - attempts: 3 - register: job_definition_create_result - -- name: show results - debug: var=job_definition_create_result -''' - -RETURN = ''' ---- -output: - description: "returns what action was taken, whether something was changed, invocation and response" - returned: always - sample: - aws_batch_job_definition_action: none - changed: false - response: - job_definition_arn: "arn:aws:batch:...." - job_definition_name: <name> - status: INACTIVE - type: container - type: dict -''' - -from ansible.module_utils.aws.batch import cc, set_api_params -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - - -# --------------------------------------------------------------------------------------------------- -# -# Helper Functions & classes -# -# --------------------------------------------------------------------------------------------------- - -# logger = logging.getLogger() -# logging.basicConfig(filename='ansible_debug.log') -# logger.setLevel(logging.DEBUG) - - -def validate_params(module, batch_client): - """ - Performs basic parameter validation. - - :param module: - :param batch_client: - :return: - """ - return - - -# --------------------------------------------------------------------------------------------------- -# -# Batch Job Definition functions -# -# --------------------------------------------------------------------------------------------------- - -def get_current_job_definition(module, batch_client): - try: - environments = batch_client.describe_job_definitions( - jobDefinitionName=module.params['job_definition_name'] - ) - if len(environments['jobDefinitions']) > 0: - latest_revision = max(map(lambda d: d['revision'], environments['jobDefinitions'])) - latest_definition = next((x for x in environments['jobDefinitions'] if x['revision'] == latest_revision), - None) - return latest_definition - return None - except ClientError: - return None - - -def create_job_definition(module, batch_client): - """ - Adds a Batch job definition - - :param module: - :param batch_client: - :return: - """ - - changed = False - - # set API parameters - api_params = set_api_params(module, get_base_params()) - container_properties_params = set_api_params(module, get_container_property_params()) - retry_strategy_params = set_api_params(module, get_retry_strategy_params()) - - api_params['retryStrategy'] = retry_strategy_params - api_params['containerProperties'] = container_properties_params - - try: - if not module.check_mode: - batch_client.register_job_definition(**api_params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Error registering job definition') - - return changed - - -def get_retry_strategy_params(): - return 'attempts', - - -def get_container_property_params(): - return ('image', 'vcpus', 'memory', 'command', 'job_role_arn', 'volumes', 'environment', 'mount_points', - 'readonly_root_filesystem', 'privileged', 'ulimits', 'user') - - -def get_base_params(): - return 'job_definition_name', 'type', 'parameters' - - -def get_compute_environment_order_list(module): - compute_environment_order_list = [] - for ceo in module.params['compute_environment_order']: - compute_environment_order_list.append(dict(order=ceo['order'], computeEnvironment=ceo['compute_environment'])) - return compute_environment_order_list - - -def remove_job_definition(module, batch_client): - """ - Remove a Batch job definition - - :param module: - :param batch_client: - :return: - """ - - changed = False - - try: - if not module.check_mode: - batch_client.deregister_job_definition(jobDefinition=module.params['job_definition_arn']) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Error removing job definition') - return changed - - -def job_definition_equal(module, current_definition): - equal = True - - for param in get_base_params(): - if module.params.get(param) != current_definition.get(cc(param)): - equal = False - break - - for param in get_container_property_params(): - if module.params.get(param) != current_definition.get('containerProperties').get(cc(param)): - equal = False - break - - for param in get_retry_strategy_params(): - if module.params.get(param) != current_definition.get('retryStrategy').get(cc(param)): - equal = False - break - - return equal - - -def manage_state(module, batch_client): - changed = False - current_state = 'absent' - state = module.params['state'] - job_definition_name = module.params['job_definition_name'] - action_taken = 'none' - response = None - - check_mode = module.check_mode - - # check if the job definition exists - current_job_definition = get_current_job_definition(module, batch_client) - if current_job_definition: - current_state = 'present' - - if state == 'present': - if current_state == 'present': - # check if definition has changed and register a new version if necessary - if not job_definition_equal(module, current_job_definition): - create_job_definition(module, batch_client) - action_taken = 'updated with new version' - changed = True - else: - # Create Job definition - changed = create_job_definition(module, batch_client) - action_taken = 'added' - - response = get_current_job_definition(module, batch_client) - if not response: - module.fail_json(msg='Unable to get job definition information after creating/updating') - else: - if current_state == 'present': - # remove the Job definition - changed = remove_job_definition(module, batch_client) - action_taken = 'deregistered' - return dict(changed=changed, batch_job_definition_action=action_taken, response=response) - - -# --------------------------------------------------------------------------------------------------- -# -# MAIN -# -# --------------------------------------------------------------------------------------------------- - -def main(): - """ - Main entry point. - - :return dict: ansible facts - """ - - argument_spec = dict( - state=dict(required=False, default='present', choices=['present', 'absent']), - job_definition_name=dict(required=True), - job_definition_arn=dict(), - type=dict(required=True), - parameters=dict(type='dict'), - image=dict(required=True), - vcpus=dict(type='int', required=True), - memory=dict(type='int', required=True), - command=dict(type='list', default=[]), - job_role_arn=dict(), - volumes=dict(type='list', default=[]), - environment=dict(type='list', default=[]), - mount_points=dict(type='list', default=[]), - readonly_root_filesystem=dict(), - privileged=dict(), - ulimits=dict(type='list', default=[]), - user=dict(), - attempts=dict(type='int') - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - batch_client = module.client('batch') - - validate_params(module, batch_client) - - results = manage_state(module, batch_client) - - module.exit_json(**camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_batch_job_queue.py b/lib/ansible/modules/cloud/amazon/aws_batch_job_queue.py deleted file mode 100644 index 9c61c69efe..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_batch_job_queue.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Jon Meran <jonathan.meran@sonos.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_batch_job_queue -short_description: Manage AWS Batch Job Queues -description: - - This module allows the management of AWS Batch Job Queues. - It is idempotent and supports "Check" mode. Use module M(aws_batch_compute_environment) to manage the compute - environment, M(aws_batch_job_queue) to manage job queues, M(aws_batch_job_definition) to manage job definitions. - -version_added: "2.5" - -author: Jon Meran (@jonmer85) -options: - job_queue_name: - description: - - The name for the job queue - required: true - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - job_queue_state: - description: - - The state of the job queue. If the job queue state is ENABLED , it is able to accept jobs. - default: "ENABLED" - choices: ["ENABLED", "DISABLED"] - type: str - priority: - description: - - The priority of the job queue. Job queues with a higher priority (or a lower integer value for the priority - parameter) are evaluated first when associated with same compute environment. Priority is determined in - ascending order, for example, a job queue with a priority value of 1 is given scheduling preference over a job - queue with a priority value of 10. - required: true - type: int - compute_environment_order: - description: - - The set of compute environments mapped to a job queue and their order relative to each other. The job - scheduler uses this parameter to determine which compute environment should execute a given job. Compute - environments must be in the VALID state before you can associate them with a job queue. You can associate up to - 3 compute environments with a job queue. - required: true - type: list - elements: dict - suboptions: - order: - type: int - description: The relative priority of the environment. - compute_environment: - type: str - description: The name of the compute environment. -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -- hosts: localhost - gather_facts: no - vars: - state: present - tasks: - - name: My Batch Job Queue - aws_batch_job_queue: - job_queue_name: jobQueueName - state: present - region: us-east-1 - job_queue_state: ENABLED - priority: 1 - compute_environment_order: - - order: 1 - compute_environment: my_compute_env1 - - order: 2 - compute_environment: my_compute_env2 - register: batch_job_queue_action - - - name: show results - debug: - var: batch_job_queue_action -''' - -RETURN = ''' ---- -output: - description: "returns what action was taken, whether something was changed, invocation and response" - returned: always - sample: - batch_job_queue_action: updated - changed: false - response: - job_queue_arn: "arn:aws:batch:...." - job_queue_name: <name> - priority: 1 - state: DISABLED - status: UPDATING - status_reason: "JobQueue Healthy" - type: dict -''' - -from ansible.module_utils.aws.batch import set_api_params -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # Handled by AnsibleAWSModule - -# --------------------------------------------------------------------------------------------------- -# -# Helper Functions & classes -# -# --------------------------------------------------------------------------------------------------- - - -def validate_params(module): - """ - Performs basic parameter validation. - - :param module: - """ - return - - -# --------------------------------------------------------------------------------------------------- -# -# Batch Job Queue functions -# -# --------------------------------------------------------------------------------------------------- - -def get_current_job_queue(module, client): - try: - environments = client.describe_job_queues( - jobQueues=[module.params['job_queue_name']] - ) - return environments['jobQueues'][0] if len(environments['jobQueues']) > 0 else None - except ClientError: - return None - - -def create_job_queue(module, client): - """ - Adds a Batch job queue - - :param module: - :param client: - :return: - """ - - changed = False - - # set API parameters - params = ('job_queue_name', 'priority') - api_params = set_api_params(module, params) - - if module.params['job_queue_state'] is not None: - api_params['state'] = module.params['job_queue_state'] - - api_params['computeEnvironmentOrder'] = get_compute_environment_order_list(module) - - try: - if not module.check_mode: - client.create_job_queue(**api_params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Error creating compute environment') - - return changed - - -def get_compute_environment_order_list(module): - compute_environment_order_list = [] - for ceo in module.params['compute_environment_order']: - compute_environment_order_list.append(dict(order=ceo['order'], computeEnvironment=ceo['compute_environment'])) - return compute_environment_order_list - - -def remove_job_queue(module, client): - """ - Remove a Batch job queue - - :param module: - :param client: - :return: - """ - - changed = False - - # set API parameters - api_params = {'jobQueue': module.params['job_queue_name']} - - try: - if not module.check_mode: - client.delete_job_queue(**api_params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Error removing job queue') - return changed - - -def manage_state(module, client): - changed = False - current_state = 'absent' - state = module.params['state'] - job_queue_state = module.params['job_queue_state'] - job_queue_name = module.params['job_queue_name'] - priority = module.params['priority'] - action_taken = 'none' - response = None - - check_mode = module.check_mode - - # check if the job queue exists - current_job_queue = get_current_job_queue(module, client) - if current_job_queue: - current_state = 'present' - - if state == 'present': - if current_state == 'present': - updates = False - # Update Batch Job Queue configuration - job_kwargs = {'jobQueue': job_queue_name} - - # Update configuration if needed - if job_queue_state and current_job_queue['state'] != job_queue_state: - job_kwargs.update({'state': job_queue_state}) - updates = True - if priority is not None and current_job_queue['priority'] != priority: - job_kwargs.update({'priority': priority}) - updates = True - - new_compute_environment_order_list = get_compute_environment_order_list(module) - if new_compute_environment_order_list != current_job_queue['computeEnvironmentOrder']: - job_kwargs['computeEnvironmentOrder'] = new_compute_environment_order_list - updates = True - - if updates: - try: - if not check_mode: - client.update_job_queue(**job_kwargs) - changed = True - action_taken = "updated" - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update job queue") - - else: - # Create Job Queue - changed = create_job_queue(module, client) - action_taken = 'added' - - # Describe job queue - response = get_current_job_queue(module, client) - if not response: - module.fail_json(msg='Unable to get job queue information after creating/updating') - else: - if current_state == 'present': - # remove the Job Queue - changed = remove_job_queue(module, client) - action_taken = 'deleted' - return dict(changed=changed, batch_job_queue_action=action_taken, response=response) - - -# --------------------------------------------------------------------------------------------------- -# -# MAIN -# -# --------------------------------------------------------------------------------------------------- - -def main(): - """ - Main entry point. - - :return dict: changed, batch_job_queue_action, response - """ - - argument_spec = dict( - state=dict(required=False, default='present', choices=['present', 'absent']), - job_queue_name=dict(required=True), - job_queue_state=dict(required=False, default='ENABLED', choices=['ENABLED', 'DISABLED']), - priority=dict(type='int', required=True), - compute_environment_order=dict(type='list', required=True), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - client = module.client('batch') - - validate_params(module) - - results = manage_state(module, client) - - module.exit_json(**camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_codebuild.py b/lib/ansible/modules/cloud/amazon/aws_codebuild.py deleted file mode 100644 index 837e22e005..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_codebuild.py +++ /dev/null @@ -1,408 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_codebuild -short_description: Create or delete an AWS CodeBuild project -notes: - - For details of the parameters and returns see U(http://boto3.readthedocs.io/en/latest/reference/services/codebuild.html). -description: - - Create or delete a CodeBuild projects on AWS, used for building code artifacts from source code. -version_added: "2.9" -author: - - Stefan Horning (@stefanhorning) <horning@mediapeers.com> -requirements: [ botocore, boto3 ] -options: - name: - description: - - Name of the CodeBuild project. - required: true - type: str - description: - description: - - Descriptive text of the CodeBuild project. - type: str - source: - description: - - Configure service and location for the build input source. - required: true - suboptions: - type: - description: - - "The type of the source. Allows one of these: C(CODECOMMIT), C(CODEPIPELINE), C(GITHUB), C(S3), C(BITBUCKET), C(GITHUB_ENTERPRISE)." - required: true - type: str - location: - description: - - Information about the location of the source code to be built. For type CODEPIPELINE location should not be specified. - type: str - git_clone_depth: - description: - - When using git you can specify the clone depth as an integer here. - type: int - buildspec: - description: - - The build spec declaration to use for the builds in this build project. Leave empty if part of the code project. - type: str - insecure_ssl: - description: - - Enable this flag to ignore SSL warnings while connecting to the project source code. - type: bool - type: dict - artifacts: - description: - - Information about the build output artifacts for the build project. - required: true - suboptions: - type: - description: - - "The type of build output for artifacts. Can be one of the following: C(CODEPIPELINE), C(NO_ARTIFACTS), C(S3)." - required: true - location: - description: - - Information about the build output artifact location. When choosing type S3, set the bucket name here. - path: - description: - - Along with namespace_type and name, the pattern that AWS CodeBuild will use to name and store the output artifacts. - - Used for path in S3 bucket when type is C(S3). - namespace_type: - description: - - Along with path and name, the pattern that AWS CodeBuild will use to determine the name and location to store the output artifacts. - - Accepts C(BUILD_ID) and C(NONE). - - "See docs here: U(http://boto3.readthedocs.io/en/latest/reference/services/codebuild.html#CodeBuild.Client.create_project)." - name: - description: - - Along with path and namespace_type, the pattern that AWS CodeBuild will use to name and store the output artifact. - packaging: - description: - - The type of build output artifact to create on S3, can be NONE for creating a folder or ZIP for a ZIP file. - type: dict - cache: - description: - - Caching params to speed up following builds. - suboptions: - type: - description: - - Cache type. Can be C(NO_CACHE) or C(S3). - required: true - location: - description: - - Caching location on S3. - required: true - type: dict - environment: - description: - - Information about the build environment for the build project. - suboptions: - type: - description: - - The type of build environment to use for the project. Usually C(LINUX_CONTAINER). - required: true - image: - description: - - The ID of the Docker image to use for this build project. - required: true - compute_type: - description: - - Information about the compute resources the build project will use. - - "Available values include: C(BUILD_GENERAL1_SMALL), C(BUILD_GENERAL1_MEDIUM), C(BUILD_GENERAL1_LARGE)." - required: true - environment_variables: - description: - - A set of environment variables to make available to builds for the build project. List of dictionaries with name and value fields. - - "Example: { name: 'MY_ENV_VARIABLE', value: 'test' }" - privileged_mode: - description: - - Enables running the Docker daemon inside a Docker container. Set to true only if the build project is be used to build Docker images. - type: dict - service_role: - description: - - The ARN of the AWS IAM role that enables AWS CodeBuild to interact with dependent AWS services on behalf of the AWS account. - type: str - timeout_in_minutes: - description: - - How long CodeBuild should wait until timing out any build that has not been marked as completed. - default: 60 - type: int - encryption_key: - description: - - The AWS Key Management Service (AWS KMS) customer master key (CMK) to be used for encrypting the build output artifacts. - type: str - tags: - description: - - A set of tags for the build project. - type: list - elements: dict - suboptions: - key: - description: The name of the Tag. - type: str - value: - description: The value of the Tag. - type: str - vpc_config: - description: - - The VPC config enables AWS CodeBuild to access resources in an Amazon VPC. - type: dict - state: - description: - - Create or remove code build project. - default: 'present' - choices: ['present', 'absent'] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- aws_codebuild: - name: my_project - description: My nice little project - service_role: "arn:aws:iam::123123:role/service-role/code-build-service-role" - source: - # Possible values: BITBUCKET, CODECOMMIT, CODEPIPELINE, GITHUB, S3 - type: CODEPIPELINE - buildspec: '' - artifacts: - namespaceType: NONE - packaging: NONE - type: CODEPIPELINE - name: my_project - environment: - computeType: BUILD_GENERAL1_SMALL - privilegedMode: "true" - image: "aws/codebuild/docker:17.09.0" - type: LINUX_CONTAINER - environmentVariables: - - { name: 'PROFILE', value: 'staging' } - encryption_key: "arn:aws:kms:us-east-1:123123:alias/aws/s3" - region: us-east-1 - state: present -''' - -RETURN = ''' -project: - description: Returns the dictionary describing the code project configuration. - returned: success - type: complex - contains: - name: - description: Name of the CodeBuild project - returned: always - type: str - sample: my_project - arn: - description: ARN of the CodeBuild project - returned: always - type: str - sample: arn:aws:codebuild:us-east-1:123123123:project/vod-api-app-builder - description: - description: A description of the build project - returned: always - type: str - sample: My nice little project - source: - description: Information about the build input source code. - returned: always - type: complex - contains: - type: - description: The type of the repository - returned: always - type: str - sample: CODEPIPELINE - location: - description: Location identifier, depending on the source type. - returned: when configured - type: str - git_clone_depth: - description: The git clone depth - returned: when configured - type: int - build_spec: - description: The build spec declaration to use for the builds in this build project. - returned: always - type: str - auth: - description: Information about the authorization settings for AWS CodeBuild to access the source code to be built. - returned: when configured - type: complex - insecure_ssl: - description: True if set to ignore SSL warnings. - returned: when configured - type: bool - artifacts: - description: Information about the output of build artifacts - returned: always - type: complex - contains: - type: - description: The type of build artifact. - returned: always - type: str - sample: CODEPIPELINE - location: - description: Output location for build artifacts - returned: when configured - type: str - # and more... see http://boto3.readthedocs.io/en/latest/reference/services/codebuild.html#CodeBuild.Client.create_project - cache: - description: Cache settings for the build project. - returned: when configured - type: dict - environment: - description: Environment settings for the build - returned: always - type: dict - service_role: - description: IAM role to be used during build to access other AWS services. - returned: always - type: str - sample: arn:aws:iam::123123123:role/codebuild-service-role - timeout_in_minutes: - description: The timeout of a build in minutes - returned: always - type: int - sample: 60 - tags: - description: Tags added to the project - returned: when configured - type: list - created: - description: Timestamp of the create time of the project - returned: always - type: str - sample: "2018-04-17T16:56:03.245000+02:00" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule, get_boto3_client_method_parameters -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict - - -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - - -def create_or_update_project(client, params, module): - resp = {} - name = params['name'] - # clean up params - formatted_params = snake_dict_to_camel_dict(dict((k, v) for k, v in params.items() if v is not None)) - permitted_create_params = get_boto3_client_method_parameters(client, 'create_project') - permitted_update_params = get_boto3_client_method_parameters(client, 'update_project') - - formatted_create_params = dict((k, v) for k, v in formatted_params.items() if k in permitted_create_params) - formatted_update_params = dict((k, v) for k, v in formatted_params.items() if k in permitted_update_params) - - # Check if project with that name already exists and if so update existing: - found = describe_project(client=client, name=name, module=module) - changed = False - - if 'name' in found: - found_project = found - resp = update_project(client=client, params=formatted_update_params, module=module) - updated_project = resp['project'] - - # Prep both dicts for sensible change comparison: - found_project.pop('lastModified') - updated_project.pop('lastModified') - if 'tags' not in updated_project: - updated_project['tags'] = [] - - if updated_project != found_project: - changed = True - return resp, changed - # Or create new project: - try: - resp = client.create_project(**formatted_create_params) - changed = True - return resp, changed - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to create CodeBuild project") - - -def update_project(client, params, module): - name = params['name'] - - try: - resp = client.update_project(**params) - return resp - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to update CodeBuild project") - - -def delete_project(client, name, module): - found = describe_project(client=client, name=name, module=module) - changed = False - if 'name' in found: - # Mark as changed when a project with that name existed before calling delete - changed = True - try: - resp = client.delete_project(name=name) - return resp, changed - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to delete CodeBuild project") - - -def describe_project(client, name, module): - project = {} - try: - projects = client.batch_get_projects(names=[name])['projects'] - if len(projects) > 0: - project = projects[0] - return project - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to describe CodeBuild projects") - - -def main(): - argument_spec = dict( - name=dict(required=True), - description=dict(), - source=dict(required=True, type='dict'), - artifacts=dict(required=True, type='dict'), - cache=dict(type='dict'), - environment=dict(type='dict'), - service_role=dict(), - timeout_in_minutes=dict(type='int', default=60), - encryption_key=dict(), - tags=dict(type='list'), - vpc_config=dict(type='dict'), - state=dict(choices=['present', 'absent'], default='present') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - client_conn = module.client('codebuild') - - state = module.params.get('state') - changed = False - - if state == 'present': - project_result, changed = create_or_update_project( - client=client_conn, - params=module.params, - module=module) - elif state == 'absent': - project_result, changed = delete_project(client=client_conn, name=module.params['name'], module=module) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(project_result)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_codecommit.py b/lib/ansible/modules/cloud/amazon/aws_codecommit.py deleted file mode 100644 index 51b752a38a..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_codecommit.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2018, Shuang Wang <ooocamel@icloud.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: aws_codecommit -version_added: "2.8" -short_description: Manage repositories in AWS CodeCommit -description: - - Supports creation and deletion of CodeCommit repositories. - - See U(https://aws.amazon.com/codecommit/) for more information about CodeCommit. -author: Shuang Wang (@ptux) - -requirements: - - botocore - - boto3 - - python >= 2.6 - -options: - name: - description: - - name of repository. - required: true - type: str - description: - description: - - description or comment of repository. - required: false - aliases: - - comment - type: str - state: - description: - - Specifies the state of repository. - required: true - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -RETURN = ''' -repository_metadata: - description: "Information about the repository." - returned: always - type: complex - contains: - account_id: - description: "The ID of the AWS account associated with the repository." - returned: when state is present - type: str - sample: "268342293637" - arn: - description: "The Amazon Resource Name (ARN) of the repository." - returned: when state is present - type: str - sample: "arn:aws:codecommit:ap-northeast-1:268342293637:username" - clone_url_http: - description: "The URL to use for cloning the repository over HTTPS." - returned: when state is present - type: str - sample: "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/reponame" - clone_url_ssh: - description: "The URL to use for cloning the repository over SSH." - returned: when state is present - type: str - sample: "ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/reponame" - creation_date: - description: "The date and time the repository was created, in timestamp format." - returned: when state is present - type: str - sample: "2018-10-16T13:21:41.261000+09:00" - last_modified_date: - description: "The date and time the repository was last modified, in timestamp format." - returned: when state is present - type: str - sample: "2018-10-16T13:21:41.261000+09:00" - repository_description: - description: "A comment or description about the repository." - returned: when state is present - type: str - sample: "test from ptux" - repository_id: - description: "The ID of the repository that was created or deleted" - returned: always - type: str - sample: "e62a5c54-i879-497b-b62f-9f99e4ebfk8e" - repository_name: - description: "The repository's name." - returned: when state is present - type: str - sample: "reponame" - -response_metadata: - description: "Information about the response." - returned: always - type: complex - contains: - http_headers: - description: "http headers of http response" - returned: always - type: dict - http_status_code: - description: "http status code of http response" - returned: always - type: str - sample: "200" - request_id: - description: "http request id" - returned: always - type: str - sample: "fb49cfca-d0fa-11e8-85cb-b3cc4b5045ef" - retry_attempts: - description: "numbers of retry attempts" - returned: always - type: str - sample: "0" -''' - -EXAMPLES = ''' -# Create a new repository -- aws_codecommit: - name: repo - state: present - -# Delete a repository -- aws_codecommit: - name: repo - state: absent -''' - -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -class CodeCommit(object): - def __init__(self, module=None): - self._module = module - self._client = self._module.client('codecommit') - self._check_mode = self._module.check_mode - - def process(self): - result = dict(changed=False) - - if self._module.params['state'] == 'present': - if not self._repository_exists(): - if not self._check_mode: - result = self._create_repository() - result['changed'] = True - else: - metadata = self._get_repository()['repositoryMetadata'] - if metadata['repositoryDescription'] != self._module.params['description']: - if not self._check_mode: - self._update_repository() - result['changed'] = True - result.update(self._get_repository()) - if self._module.params['state'] == 'absent' and self._repository_exists(): - if not self._check_mode: - result = self._delete_repository() - result['changed'] = True - return result - - def _repository_exists(self): - try: - paginator = self._client.get_paginator('list_repositories') - for page in paginator.paginate(): - repositories = page['repositories'] - for item in repositories: - if self._module.params['name'] in item.values(): - return True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="couldn't get repository") - return False - - def _get_repository(self): - try: - result = self._client.get_repository( - repositoryName=self._module.params['name'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="couldn't get repository") - return result - - def _update_repository(self): - try: - result = self._client.update_repository_description( - repositoryName=self._module.params['name'], - repositoryDescription=self._module.params['description'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="couldn't create repository") - return result - - def _create_repository(self): - try: - result = self._client.create_repository( - repositoryName=self._module.params['name'], - repositoryDescription=self._module.params['description'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="couldn't create repository") - return result - - def _delete_repository(self): - try: - result = self._client.delete_repository( - repositoryName=self._module.params['name'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="couldn't delete repository") - return result - - -def main(): - argument_spec = dict( - name=dict(required=True), - state=dict(choices=['present', 'absent'], required=True), - description=dict(default='', aliases=['comment']) - ) - - ansible_aws_module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - aws_codecommit = CodeCommit(module=ansible_aws_module) - result = aws_codecommit.process() - ansible_aws_module.exit_json(**camel_dict_to_snake_dict(result)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_codepipeline.py b/lib/ansible/modules/cloud/amazon/aws_codepipeline.py deleted file mode 100644 index 3d3d683445..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_codepipeline.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_codepipeline -short_description: Create or delete AWS CodePipelines -notes: - - for details of the parameters and returns see U(http://boto3.readthedocs.io/en/latest/reference/services/codepipeline.html) -description: - - Create or delete a CodePipeline on AWS. -version_added: "2.9" -author: - - Stefan Horning (@stefanhorning) <horning@mediapeers.com> -requirements: [ botocore, boto3 ] -options: - name: - description: - - Name of the pipeline - required: true - type: str - role_arn: - description: - - ARN of the IAM role to use when executing the pipeline - required: true - type: str - artifact_store: - description: - - Location information where artifacts are stored (on S3). Dictionary with fields type and location. - required: true - suboptions: - type: - description: - - Type of the artifacts storage (only 'S3' is currently supported). - type: str - location: - description: - - Bucket name for artifacts. - type: str - type: dict - stages: - description: - - List of stages to perform in the CodePipeline. List of dictionaries containing name and actions for each stage. - required: true - suboptions: - name: - description: - - Name of the stage (step) in the codepipeline - type: str - actions: - description: - - List of action configurations for that stage. - - 'See the boto3 documentation for full documentation of suboptions:' - - 'U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codepipeline.html#CodePipeline.Client.create_pipeline)' - type: list - elements: dict - elements: dict - type: list - version: - description: - - Version number of the pipeline. This number is automatically incremented when a pipeline is updated. - required: false - type: int - state: - description: - - Create or remove code pipeline - default: 'present' - choices: ['present', 'absent'] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Example for creating a pipeline for continuous deploy of Github code to an ECS cluster (container) -- aws_codepipeline: - name: my_deploy_pipeline - role_arn: arn:aws:iam::123456:role/AWS-CodePipeline-Service - artifact_store: - type: S3 - location: my_s3_codepipline_bucket - stages: - - name: Get_source - actions: - - - name: Git_pull - actionTypeId: - category: Source - owner: ThirdParty - provider: GitHub - version: '1' - outputArtifacts: - - { name: my-app-source } - configuration: - Owner: mediapeers - Repo: my_gh_repo - PollForSourceChanges: 'true' - Branch: master - # Generate token like this: - # https://docs.aws.amazon.com/codepipeline/latest/userguide/GitHub-rotate-personal-token-CLI.html - # GH Link: https://github.com/settings/tokens - OAuthToken: 'abc123def456' - runOrder: 1 - - name: Build - actions: - - - name: CodeBuild - actionTypeId: - category: Build - owner: AWS - provider: CodeBuild - version: '1' - inputArtifacts: - - { name: my-app-source } - outputArtifacts: - - { name: my-app-build } - configuration: - # A project with that name needs to be setup on AWS CodeBuild already (use code_build module). - ProjectName: codebuild-project-name - runOrder: 1 - - name: ECS_deploy - actions: - - - name: ECS_deploy - actionTypeId: - category: Deploy - owner: AWS - provider: ECS - version: '1' - inputArtifacts: - - { name: vod-api-app-build } - configuration: - # an ECS cluster with that name needs to be setup on AWS ECS already (use ecs_cluster and ecs_service module) - ClusterName: ecs-cluster-name - ServiceName: ecs-cluster-service-name - FileName: imagedefinitions.json - region: us-east-1 - state: present -''' - -RETURN = ''' -pipeline: - description: Returns the dictionary describing the code pipeline configuration. - returned: success - type: complex - contains: - name: - description: Name of the CodePipeline - returned: always - type: str - sample: my_deploy_pipeline - role_arn: - description: ARN of the IAM role attached to the code pipeline - returned: always - type: str - sample: arn:aws:iam::123123123:role/codepipeline-service-role - artifact_store: - description: Information about where the build artifacts are stored - returned: always - type: complex - contains: - type: - description: The type of the artifacts store, such as S3 - returned: always - type: str - sample: S3 - location: - description: The location of the artifacts storage (s3 bucket name) - returned: always - type: str - sample: my_s3_codepipline_bucket - encryption_key: - description: The encryption key used to encrypt the artifacts store, such as an AWS KMS key. - returned: when configured - type: str - stages: - description: List of stages configured for this pipeline - returned: always - type: list - version: - description: The version number of the pipeline. This number is auto incremented when pipeline params are changed. - returned: always - type: int -''' - -import copy -import traceback - -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, compare_policies - - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -def create_pipeline(client, name, role_arn, artifact_store, stages, version, module): - pipeline_dict = {'name': name, 'roleArn': role_arn, 'artifactStore': artifact_store, 'stages': stages} - if version: - pipeline_dict['version'] = version - try: - resp = client.create_pipeline(pipeline=pipeline_dict) - return resp - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable create pipeline {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to create pipeline {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc()) - - -def update_pipeline(client, pipeline_dict, module): - try: - resp = client.update_pipeline(pipeline=pipeline_dict) - return resp - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable update pipeline {0}: {1}".format(pipeline_dict['name'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to update pipeline {0}: {1}".format(pipeline_dict['name'], to_native(e)), - exception=traceback.format_exc()) - - -def delete_pipeline(client, name, module): - try: - resp = client.delete_pipeline(name=name) - return resp - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable delete pipeline {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to delete pipeline {0}: {1}".format(name, to_native(e)), - exception=traceback.format_exc()) - - -def describe_pipeline(client, name, version, module): - pipeline = {} - try: - if version is not None: - pipeline = client.get_pipeline(name=name, version=version) - return pipeline - else: - pipeline = client.get_pipeline(name=name) - return pipeline - except is_boto3_error_code('PipelineNotFoundException'): - return pipeline - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - - -def main(): - argument_spec = dict( - name=dict(required=True, type='str'), - role_arn=dict(required=True, type='str'), - artifact_store=dict(required=True, type='dict'), - stages=dict(required=True, type='list'), - version=dict(type='int'), - state=dict(choices=['present', 'absent'], default='present') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - client_conn = module.client('codepipeline') - - state = module.params.get('state') - changed = False - - # Determine if the CodePipeline exists - found_code_pipeline = describe_pipeline(client=client_conn, name=module.params['name'], version=module.params['version'], module=module) - pipeline_result = {} - - if state == 'present': - if 'pipeline' in found_code_pipeline: - pipeline_dict = copy.deepcopy(found_code_pipeline['pipeline']) - # Update dictionary with provided module params: - pipeline_dict['roleArn'] = module.params['role_arn'] - pipeline_dict['artifactStore'] = module.params['artifact_store'] - pipeline_dict['stages'] = module.params['stages'] - if module.params['version'] is not None: - pipeline_dict['version'] = module.params['version'] - - pipeline_result = update_pipeline(client=client_conn, pipeline_dict=pipeline_dict, module=module) - - if compare_policies(found_code_pipeline['pipeline'], pipeline_result['pipeline']): - changed = True - else: - pipeline_result = create_pipeline( - client=client_conn, - name=module.params['name'], - role_arn=module.params['role_arn'], - artifact_store=module.params['artifact_store'], - stages=module.params['stages'], - version=module.params['version'], - module=module) - changed = True - elif state == 'absent': - if found_code_pipeline: - pipeline_result = delete_pipeline(client=client_conn, name=module.params['name'], module=module) - changed = True - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(pipeline_result)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_config_aggregation_authorization.py b/lib/ansible/modules/cloud/amazon/aws_config_aggregation_authorization.py deleted file mode 100644 index 687e5cb125..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_config_aggregation_authorization.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_config_aggregation_authorization -short_description: Manage cross-account AWS Config authorizations -description: - - Module manages AWS Config resources. -version_added: "2.6" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - state: - description: - - Whether the Config rule should be present or absent. - default: present - choices: ['present', 'absent'] - type: str - authorized_account_id: - description: - - The 12-digit account ID of the account authorized to aggregate data. - type: str - required: true - authorized_aws_region: - description: - - The region authorized to collect aggregated data. - type: str - required: true -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Get current account ID - aws_caller_info: - register: whoami -- aws_config_aggregation_authorization: - state: present - authorized_account_id: '{{ whoami.account }}' - authorzed_aws_region: us-east-1 -''' - -RETURN = '''#''' - - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry - - -def resource_exists(client, module, params): - try: - current_authorizations = client.describe_aggregation_authorizations()['AggregationAuthorizations'] - authorization_exists = next( - (item for item in current_authorizations if item["AuthorizedAccountId"] == params['AuthorizedAccountId']), - None - ) - if authorization_exists: - return True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError): - return False - - -def create_resource(client, module, params, result): - try: - response = client.put_aggregation_authorization( - AuthorizedAccountId=params['AuthorizedAccountId'], - AuthorizedAwsRegion=params['AuthorizedAwsRegion'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Aggregation authorization") - - -def update_resource(client, module, params, result): - current_authorizations = client.describe_aggregation_authorizations()['AggregationAuthorizations'] - current_params = next( - (item for item in current_authorizations if item["AuthorizedAccountId"] == params['AuthorizedAccountId']), - None - ) - - del current_params['AggregationAuthorizationArn'] - del current_params['CreationTime'] - - if params != current_params: - try: - response = client.put_aggregation_authorization( - AuthorizedAccountId=params['AuthorizedAccountId'], - AuthorizedAwsRegion=params['AuthorizedAwsRegion'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Aggregation authorization") - - -def delete_resource(client, module, params, result): - try: - response = client.delete_aggregation_authorization( - AuthorizedAccountId=params['AuthorizedAccountId'], - AuthorizedAwsRegion=params['AuthorizedAwsRegion'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete AWS Aggregation authorization") - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'state': dict(type='str', choices=['present', 'absent'], default='present'), - 'authorized_account_id': dict(type='str', required=True), - 'authorized_aws_region': dict(type='str', required=True), - }, - supports_check_mode=False, - ) - - result = {'changed': False} - - params = { - 'AuthorizedAccountId': module.params.get('authorized_account_id'), - 'AuthorizedAwsRegion': module.params.get('authorized_aws_region'), - } - - client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) - resource_status = resource_exists(client, module, params) - - if module.params.get('state') == 'present': - if not resource_status: - create_resource(client, module, params, result) - else: - update_resource(client, module, params, result) - - if module.params.get('state') == 'absent': - if resource_status: - delete_resource(client, module, params, result) - - module.exit_json(changed=result['changed']) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_config_aggregator.py b/lib/ansible/modules/cloud/amazon/aws_config_aggregator.py deleted file mode 100644 index 7108a47f02..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_config_aggregator.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_config_aggregator -short_description: Manage AWS Config aggregations across multiple accounts -description: - - Module manages AWS Config resources -version_added: "2.6" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - name: - description: - - The name of the AWS Config resource. - required: true - type: str - state: - description: - - Whether the Config rule should be present or absent. - default: present - choices: ['present', 'absent'] - type: str - account_sources: - description: - - Provides a list of source accounts and regions to be aggregated. - suboptions: - account_ids: - description: - - A list of 12-digit account IDs of accounts being aggregated. - type: list - elements: str - aws_regions: - description: - - A list of source regions being aggregated. - type: list - elements: str - all_aws_regions: - description: - - If true, aggregate existing AWS Config regions and future regions. - type: bool - type: list - elements: dict - required: true - organization_source: - description: - - The region authorized to collect aggregated data. - suboptions: - role_arn: - description: - - ARN of the IAM role used to retrieve AWS Organization details associated with the aggregator account. - type: str - aws_regions: - description: - - The source regions being aggregated. - type: list - elements: str - all_aws_regions: - description: - - If true, aggregate existing AWS Config regions and future regions. - type: bool - type: dict - required: true -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create cross-account aggregator - aws_config_aggregator: - name: test_config_rule - state: present - account_sources: - account_ids: - - 1234567890 - - 0123456789 - - 9012345678 - all_aws_regions: yes -''' - -RETURN = '''#''' - - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict - - -def resource_exists(client, module, params): - try: - aggregator = client.describe_configuration_aggregators( - ConfigurationAggregatorNames=[params['name']] - ) - return aggregator['ConfigurationAggregators'][0] - except is_boto3_error_code('NoSuchConfigurationAggregatorException'): - return - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - - -def create_resource(client, module, params, result): - try: - client.put_configuration_aggregator( - ConfigurationAggregatorName=params['ConfigurationAggregatorName'], - AccountAggregationSources=params['AccountAggregationSources'], - OrganizationAggregationSource=params['OrganizationAggregationSource'] - ) - result['changed'] = True - result['aggregator'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Config configuration aggregator") - - -def update_resource(client, module, params, result): - current_params = client.describe_configuration_aggregators( - ConfigurationAggregatorNames=[params['name']] - ) - - del current_params['ConfigurationAggregatorArn'] - del current_params['CreationTime'] - del current_params['LastUpdatedTime'] - - if params != current_params['ConfigurationAggregators'][0]: - try: - client.put_configuration_aggregator( - ConfigurationAggregatorName=params['ConfigurationAggregatorName'], - AccountAggregationSources=params['AccountAggregationSources'], - OrganizationAggregationSource=params['OrganizationAggregationSource'] - ) - result['changed'] = True - result['aggregator'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Config configuration aggregator") - - -def delete_resource(client, module, params, result): - try: - client.delete_configuration_aggregator( - ConfigurationAggregatorName=params['ConfigurationAggregatorName'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete AWS Config configuration aggregator") - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'name': dict(type='str', required=True), - 'state': dict(type='str', choices=['present', 'absent'], default='present'), - 'account_sources': dict(type='list', required=True), - 'organization_source': dict(type='dict', required=True) - }, - supports_check_mode=False, - ) - - result = { - 'changed': False - } - - name = module.params.get('name') - state = module.params.get('state') - - params = {} - if name: - params['ConfigurationAggregatorName'] = name - if module.params.get('account_sources'): - params['AccountAggregationSources'] = [] - for i in module.params.get('account_sources'): - tmp_dict = {} - if i.get('account_ids'): - tmp_dict['AccountIds'] = i.get('account_ids') - if i.get('aws_regions'): - tmp_dict['AwsRegions'] = i.get('aws_regions') - if i.get('all_aws_regions') is not None: - tmp_dict['AllAwsRegions'] = i.get('all_aws_regions') - params['AccountAggregationSources'].append(tmp_dict) - if module.params.get('organization_source'): - params['OrganizationAggregationSource'] = {} - if module.params.get('organization_source').get('role_arn'): - params['OrganizationAggregationSource'].update({ - 'RoleArn': module.params.get('organization_source').get('role_arn') - }) - if module.params.get('organization_source').get('aws_regions'): - params['OrganizationAggregationSource'].update({ - 'AwsRegions': module.params.get('organization_source').get('aws_regions') - }) - if module.params.get('organization_source').get('all_aws_regions') is not None: - params['OrganizationAggregationSourcep'].update({ - 'AllAwsRegions': module.params.get('organization_source').get('all_aws_regions') - }) - - client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) - - resource_status = resource_exists(client, module, params) - - if state == 'present': - if not resource_status: - create_resource(client, module, params, result) - else: - update_resource(client, module, params, result) - - if state == 'absent': - if resource_status: - delete_resource(client, module, params, result) - - module.exit_json(changed=result['changed']) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_config_delivery_channel.py b/lib/ansible/modules/cloud/amazon/aws_config_delivery_channel.py deleted file mode 100644 index 9ef4802f3e..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_config_delivery_channel.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_config_delivery_channel -short_description: Manage AWS Config delivery channels -description: - - This module manages AWS Config delivery locations for rule checks and configuration info. -version_added: "2.6" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - name: - description: - - The name of the AWS Config resource. - required: true - type: str - state: - description: - - Whether the Config rule should be present or absent. - default: present - choices: ['present', 'absent'] - type: str - s3_bucket: - description: - - The name of the Amazon S3 bucket to which AWS Config delivers configuration snapshots and configuration history files. - type: str - required: true - s3_prefix: - description: - - The prefix for the specified Amazon S3 bucket. - type: str - sns_topic_arn: - description: - - The Amazon Resource Name (ARN) of the Amazon SNS topic to which AWS Config sends notifications about configuration changes. - type: str - delivery_frequency: - description: - - The frequency with which AWS Config delivers configuration snapshots. - choices: ['One_Hour', 'Three_Hours', 'Six_Hours', 'Twelve_Hours', 'TwentyFour_Hours'] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create Delivery Channel for AWS Config - aws_config_delivery_channel: - name: test_delivery_channel - state: present - s3_bucket: 'test_aws_config_bucket' - sns_topic_arn: 'arn:aws:sns:us-east-1:123456789012:aws_config_topic:1234ab56-cdef-7g89-01hi-2jk34l5m67no' - delivery_frequency: 'Twelve_Hours' -''' - -RETURN = '''#''' - - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry - - -# this waits for an IAM role to become fully available, at the cost of -# taking a long time to fail when the IAM role/policy really is invalid -retry_unavailable_iam_on_put_delivery = AWSRetry.backoff( - catch_extra_error_codes=['InsufficientDeliveryPolicyException'], -) - - -def resource_exists(client, module, params): - try: - channel = client.describe_delivery_channels( - DeliveryChannelNames=[params['name']], - aws_retry=True, - ) - return channel['DeliveryChannels'][0] - except is_boto3_error_code('NoSuchDeliveryChannelException'): - return - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - - -def create_resource(client, module, params, result): - try: - retry_unavailable_iam_on_put_delivery( - client.put_delivery_channel, - )( - DeliveryChannel=params, - ) - result['changed'] = True - result['channel'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except is_boto3_error_code('InvalidS3KeyPrefixException') as e: - module.fail_json_aws(e, msg="The `s3_prefix` parameter was invalid. Try '/' for no prefix") - except is_boto3_error_code('InsufficientDeliveryPolicyException') as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="The `s3_prefix` or `s3_bucket` parameter is invalid. " - "Make sure the bucket exists and is available") - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Couldn't create AWS Config delivery channel") - - -def update_resource(client, module, params, result): - current_params = client.describe_delivery_channels( - DeliveryChannelNames=[params['name']], - aws_retry=True, - ) - - if params != current_params['DeliveryChannels'][0]: - try: - retry_unavailable_iam_on_put_delivery( - client.put_delivery_channel, - )( - DeliveryChannel=params, - ) - result['changed'] = True - result['channel'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except is_boto3_error_code('InvalidS3KeyPrefixException') as e: - module.fail_json_aws(e, msg="The `s3_prefix` parameter was invalid. Try '/' for no prefix") - except is_boto3_error_code('InsufficientDeliveryPolicyException') as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="The `s3_prefix` or `s3_bucket` parameter is invalid. " - "Make sure the bucket exists and is available") - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Couldn't create AWS Config delivery channel") - - -def delete_resource(client, module, params, result): - try: - response = client.delete_delivery_channel( - DeliveryChannelName=params['name'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete AWS Config delivery channel") - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'name': dict(type='str', required=True), - 'state': dict(type='str', choices=['present', 'absent'], default='present'), - 's3_bucket': dict(type='str', required=True), - 's3_prefix': dict(type='str'), - 'sns_topic_arn': dict(type='str'), - 'delivery_frequency': dict( - type='str', - choices=[ - 'One_Hour', - 'Three_Hours', - 'Six_Hours', - 'Twelve_Hours', - 'TwentyFour_Hours' - ] - ), - }, - supports_check_mode=False, - ) - - result = { - 'changed': False - } - - name = module.params.get('name') - state = module.params.get('state') - - params = {} - if name: - params['name'] = name - if module.params.get('s3_bucket'): - params['s3BucketName'] = module.params.get('s3_bucket') - if module.params.get('s3_prefix'): - params['s3KeyPrefix'] = module.params.get('s3_prefix') - if module.params.get('sns_topic_arn'): - params['snsTopicARN'] = module.params.get('sns_topic_arn') - if module.params.get('delivery_frequency'): - params['configSnapshotDeliveryProperties'] = { - 'deliveryFrequency': module.params.get('delivery_frequency') - } - - client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) - - resource_status = resource_exists(client, module, params) - - if state == 'present': - if not resource_status: - create_resource(client, module, params, result) - if resource_status: - update_resource(client, module, params, result) - - if state == 'absent': - if resource_status: - delete_resource(client, module, params, result) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_config_recorder.py b/lib/ansible/modules/cloud/amazon/aws_config_recorder.py deleted file mode 100644 index d986ed0ebb..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_config_recorder.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_config_recorder -short_description: Manage AWS Config Recorders -description: - - Module manages AWS Config configuration recorder settings. -version_added: "2.6" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - name: - description: - - The name of the AWS Config resource. - required: true - type: str - state: - description: - - Whether the Config rule should be present or absent. - default: present - choices: ['present', 'absent'] - type: str - role_arn: - description: - - Amazon Resource Name (ARN) of the IAM role used to describe the AWS resources associated with the account. - - Required when I(state=present). - type: str - recording_group: - description: - - Specifies the types of AWS resources for which AWS Config records configuration changes. - - Required when I(state=present) - suboptions: - all_supported: - description: - - Specifies whether AWS Config records configuration changes for every supported type of regional resource. - - If I(all_supported=true), when AWS Config adds support for a new type of regional resource, it starts - recording resources of that type automatically. - - If I(all_supported=true), you cannot enumerate a list of I(resource_types). - include_global_types: - description: - - Specifies whether AWS Config includes all supported types of global resources (for example, IAM resources) - with the resources that it records. - - The configuration details for any global resource are the same in all regions. To prevent duplicate configuration items, - you should consider customizing AWS Config in only one region to record global resources. - - If you set I(include_global_types=true), you must also set I(all_supported=true). - - If you set I(include_global_types=true), when AWS Config adds support for a new type of global resource, it starts recording - resources of that type automatically. - resource_types: - description: - - A list that specifies the types of AWS resources for which AWS Config records configuration changes (for example, - C(AWS::EC2::Instance) or C(AWS::CloudTrail::Trail)). - - Before you can set this option, you must set I(all_supported=false). - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create Configuration Recorder for AWS Config - aws_config_recorder: - name: test_configuration_recorder - state: present - role_arn: 'arn:aws:iam::123456789012:role/AwsConfigRecorder' - recording_group: - all_supported: true - include_global_types: true -''' - -RETURN = '''#''' - - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry - - -def resource_exists(client, module, params): - try: - recorder = client.describe_configuration_recorders( - ConfigurationRecorderNames=[params['name']] - ) - return recorder['ConfigurationRecorders'][0] - except is_boto3_error_code('NoSuchConfigurationRecorderException'): - return - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - - -def create_resource(client, module, params, result): - try: - response = client.put_configuration_recorder( - ConfigurationRecorder=params - ) - result['changed'] = True - result['recorder'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Config configuration recorder") - - -def update_resource(client, module, params, result): - current_params = client.describe_configuration_recorders( - ConfigurationRecorderNames=[params['name']] - ) - - if params != current_params['ConfigurationRecorders'][0]: - try: - response = client.put_configuration_recorder( - ConfigurationRecorder=params - ) - result['changed'] = True - result['recorder'] = camel_dict_to_snake_dict(resource_exists(client, module, params)) - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't update AWS Config configuration recorder") - - -def delete_resource(client, module, params, result): - try: - response = client.delete_configuration_recorder( - ConfigurationRecorderName=params['name'] - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete AWS Config configuration recorder") - - -def main(): - - module = AnsibleAWSModule( - argument_spec={ - 'name': dict(type='str', required=True), - 'state': dict(type='str', choices=['present', 'absent'], default='present'), - 'role_arn': dict(type='str'), - 'recording_group': dict(type='dict'), - }, - supports_check_mode=False, - required_if=[ - ('state', 'present', ['role_arn', 'recording_group']), - ], - ) - - result = { - 'changed': False - } - - name = module.params.get('name') - state = module.params.get('state') - - params = {} - if name: - params['name'] = name - if module.params.get('role_arn'): - params['roleARN'] = module.params.get('role_arn') - if module.params.get('recording_group'): - params['recordingGroup'] = {} - if module.params.get('recording_group').get('all_supported') is not None: - params['recordingGroup'].update({ - 'allSupported': module.params.get('recording_group').get('all_supported') - }) - if module.params.get('recording_group').get('include_global_types') is not None: - params['recordingGroup'].update({ - 'includeGlobalResourceTypes': module.params.get('recording_group').get('include_global_types') - }) - if module.params.get('recording_group').get('resource_types'): - params['recordingGroup'].update({ - 'resourceTypes': module.params.get('recording_group').get('resource_types') - }) - else: - params['recordingGroup'].update({ - 'resourceTypes': [] - }) - - client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) - - resource_status = resource_exists(client, module, params) - - if state == 'present': - if not resource_status: - create_resource(client, module, params, result) - if resource_status: - update_resource(client, module, params, result) - - if state == 'absent': - if resource_status: - delete_resource(client, module, params, result) - - module.exit_json(changed=result['changed']) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_config_rule.py b/lib/ansible/modules/cloud/amazon/aws_config_rule.py deleted file mode 100644 index 97d39e4dcc..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_config_rule.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_config_rule -short_description: Manage AWS Config resources -description: - - Module manages AWS Config rules -version_added: "2.6" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - name: - description: - - The name of the AWS Config resource. - required: true - type: str - state: - description: - - Whether the Config rule should be present or absent. - default: present - choices: ['present', 'absent'] - type: str - description: - description: - - The description that you provide for the AWS Config rule. - type: str - scope: - description: - - Defines which resources can trigger an evaluation for the rule. - suboptions: - compliance_types: - description: - - The resource types of only those AWS resources that you want to trigger an evaluation for the rule. - You can only specify one type if you also specify a resource ID for I(compliance_id). - compliance_id: - description: - - The ID of the only AWS resource that you want to trigger an evaluation for the rule. If you specify a resource ID, - you must specify one resource type for I(compliance_types). - tag_key: - description: - - The tag key that is applied to only those AWS resources that you want to trigger an evaluation for the rule. - tag_value: - description: - - The tag value applied to only those AWS resources that you want to trigger an evaluation for the rule. - If you specify a value for I(tag_value), you must also specify a value for I(tag_key). - type: dict - source: - description: - - Provides the rule owner (AWS or customer), the rule identifier, and the notifications that cause the function to - evaluate your AWS resources. - suboptions: - owner: - description: - - The resource types of only those AWS resources that you want to trigger an evaluation for the rule. - You can only specify one type if you also specify a resource ID for I(compliance_id). - identifier: - description: - - The ID of the only AWS resource that you want to trigger an evaluation for the rule. - If you specify a resource ID, you must specify one resource type for I(compliance_types). - details: - description: - - Provides the source and type of the event that causes AWS Config to evaluate your AWS resources. - - This parameter expects a list of dictionaries. Each dictionary expects the following key/value pairs. - - Key `EventSource` The source of the event, such as an AWS service, that triggers AWS Config to evaluate your AWS resources. - - Key `MessageType` The type of notification that triggers AWS Config to run an evaluation for a rule. - - Key `MaximumExecutionFrequency` The frequency at which you want AWS Config to run evaluations for a custom rule with a periodic trigger. - type: dict - required: true - input_parameters: - description: - - A string, in JSON format, that is passed to the AWS Config rule Lambda function. - type: str - execution_frequency: - description: - - The maximum frequency with which AWS Config runs evaluations for a rule. - choices: ['One_Hour', 'Three_Hours', 'Six_Hours', 'Twelve_Hours', 'TwentyFour_Hours'] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create Config Rule for AWS Config - aws_config_rule: - name: test_config_rule - state: present - description: 'This AWS Config rule checks for public write access on S3 buckets' - scope: - compliance_types: - - 'AWS::S3::Bucket' - source: - owner: AWS - identifier: 'S3_BUCKET_PUBLIC_WRITE_PROHIBITED' - -''' - -RETURN = '''#''' - - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict - - -def rule_exists(client, module, params): - try: - rule = client.describe_config_rules( - ConfigRuleNames=[params['ConfigRuleName']], - aws_retry=True, - ) - return rule['ConfigRules'][0] - except is_boto3_error_code('NoSuchConfigRuleException'): - return - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - - -def create_resource(client, module, params, result): - try: - client.put_config_rule( - ConfigRule=params - ) - result['changed'] = True - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Config rule") - - -def update_resource(client, module, params, result): - current_params = client.describe_config_rules( - ConfigRuleNames=[params['ConfigRuleName']], - aws_retry=True, - ) - - del current_params['ConfigRules'][0]['ConfigRuleArn'] - del current_params['ConfigRules'][0]['ConfigRuleId'] - - if params != current_params['ConfigRules'][0]: - try: - client.put_config_rule( - ConfigRule=params - ) - result['changed'] = True - result['rule'] = camel_dict_to_snake_dict(rule_exists(client, module, params)) - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create AWS Config rule") - - -def delete_resource(client, module, params, result): - try: - response = client.delete_config_rule( - ConfigRuleName=params['ConfigRuleName'], - aws_retry=True, - ) - result['changed'] = True - result['rule'] = {} - return result - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete AWS Config rule") - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'name': dict(type='str', required=True), - 'state': dict(type='str', choices=['present', 'absent'], default='present'), - 'description': dict(type='str'), - 'scope': dict(type='dict'), - 'source': dict(type='dict', required=True), - 'input_parameters': dict(type='str'), - 'execution_frequency': dict( - type='str', - choices=[ - 'One_Hour', - 'Three_Hours', - 'Six_Hours', - 'Twelve_Hours', - 'TwentyFour_Hours' - ] - ), - }, - supports_check_mode=False, - ) - - result = { - 'changed': False - } - - name = module.params.get('name') - resource_type = module.params.get('resource_type') - state = module.params.get('state') - - params = {} - if name: - params['ConfigRuleName'] = name - if module.params.get('description'): - params['Description'] = module.params.get('description') - if module.params.get('scope'): - params['Scope'] = {} - if module.params.get('scope').get('compliance_types'): - params['Scope'].update({ - 'ComplianceResourceTypes': module.params.get('scope').get('compliance_types') - }) - if module.params.get('scope').get('tag_key'): - params['Scope'].update({ - 'TagKey': module.params.get('scope').get('tag_key') - }) - if module.params.get('scope').get('tag_value'): - params['Scope'].update({ - 'TagValue': module.params.get('scope').get('tag_value') - }) - if module.params.get('scope').get('compliance_id'): - params['Scope'].update({ - 'ComplianceResourceId': module.params.get('scope').get('compliance_id') - }) - if module.params.get('source'): - params['Source'] = {} - if module.params.get('source').get('owner'): - params['Source'].update({ - 'Owner': module.params.get('source').get('owner') - }) - if module.params.get('source').get('identifier'): - params['Source'].update({ - 'SourceIdentifier': module.params.get('source').get('identifier') - }) - if module.params.get('source').get('details'): - params['Source'].update({ - 'SourceDetails': module.params.get('source').get('details') - }) - if module.params.get('input_parameters'): - params['InputParameters'] = module.params.get('input_parameters') - if module.params.get('execution_frequency'): - params['MaximumExecutionFrequency'] = module.params.get('execution_frequency') - params['ConfigRuleState'] = 'ACTIVE' - - client = module.client('config', retry_decorator=AWSRetry.jittered_backoff()) - - existing_rule = rule_exists(client, module, params) - - if state == 'present': - if not existing_rule: - create_resource(client, module, params, result) - else: - update_resource(client, module, params, result) - - if state == 'absent': - if existing_rule: - delete_resource(client, module, params, result) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_direct_connect_connection.py b/lib/ansible/modules/cloud/amazon/aws_direct_connect_connection.py deleted file mode 100644 index 2d603571f8..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_direct_connect_connection.py +++ /dev/null @@ -1,343 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: aws_direct_connect_connection -short_description: Creates, deletes, modifies a DirectConnect connection -description: - - Create, update, or delete a Direct Connect connection between a network and a specific AWS Direct Connect location. - Upon creation the connection may be added to a link aggregation group or established as a standalone connection. - The connection may later be associated or disassociated with a link aggregation group. -version_added: "2.4" -author: "Sloane Hertel (@s-hertel)" -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore -options: - state: - description: - - The state of the Direct Connect connection. - choices: - - present - - absent - type: str - required: true - name: - description: - - The name of the Direct Connect connection. This is required to create a - new connection. - - One of I(connection_id) or I(name) must be specified. - type: str - connection_id: - description: - - The ID of the Direct Connect connection. - - Modifying attributes of a connection with I(forced_update) will result in a new Direct Connect connection ID. - - One of I(connection_id) or I(name) must be specified. - type: str - location: - description: - - Where the Direct Connect connection is located. - - Required when I(state=present). - type: str - bandwidth: - description: - - The bandwidth of the Direct Connect connection. - - Required when I(state=present). - choices: - - 1Gbps - - 10Gbps - type: str - link_aggregation_group: - description: - - The ID of the link aggregation group you want to associate with the connection. - - This is optional when a stand-alone connection is desired. - type: str - forced_update: - description: - - To modify bandwidth or location the connection will need to be deleted and recreated. - By default this will not happen - this option must be set to True. - type: bool -""" - -EXAMPLES = """ - -# create a Direct Connect connection -- aws_direct_connect_connection: - name: ansible-test-connection - state: present - location: EqDC2 - link_aggregation_group: dxlag-xxxxxxxx - bandwidth: 1Gbps - register: dc - -# disassociate the LAG from the connection -- aws_direct_connect_connection: - state: present - connection_id: dc.connection.connection_id - location: EqDC2 - bandwidth: 1Gbps - -# replace the connection with one with more bandwidth -- aws_direct_connect_connection: - state: present - name: ansible-test-connection - location: EqDC2 - bandwidth: 10Gbps - forced_update: True - -# delete the connection -- aws_direct_connect_connection: - state: absent - name: ansible-test-connection -""" - -RETURN = """ -connection: - description: The attributes of the direct connect connection. - type: complex - returned: I(state=present) - contains: - aws_device: - description: The endpoint which the physical connection terminates on. - returned: when the requested state is no longer 'requested' - type: str - sample: EqDC2-12pmo7hemtz1z - bandwidth: - description: The bandwidth of the connection. - returned: always - type: str - sample: 1Gbps - connection_id: - description: The ID of the connection. - returned: always - type: str - sample: dxcon-ffy9ywed - connection_name: - description: The name of the connection. - returned: always - type: str - sample: ansible-test-connection - connection_state: - description: The state of the connection. - returned: always - type: str - sample: pending - loa_issue_time: - description: The issue time of the connection's Letter of Authorization - Connecting Facility Assignment. - returned: when the LOA-CFA has been issued (the connection state will no longer be 'requested') - type: str - sample: '2018-03-20T17:36:26-04:00' - location: - description: The location of the connection. - returned: always - type: str - sample: EqDC2 - owner_account: - description: The account that owns the direct connect connection. - returned: always - type: str - sample: '123456789012' - region: - description: The region in which the connection exists. - returned: always - type: str - sample: us-east-1 -""" - -import traceback -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, AWSRetry) -from ansible.module_utils.aws.direct_connect import (DirectConnectError, delete_connection, - associate_connection_and_lag, disassociate_connection_and_lag) - -try: - from botocore.exceptions import BotoCoreError, ClientError -except Exception: - pass - # handled by imported AnsibleAWSModule - -retry_params = {"tries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} - - -def connection_status(client, connection_id): - return connection_exists(client, connection_id=connection_id, connection_name=None, verify=False) - - -def connection_exists(client, connection_id=None, connection_name=None, verify=True): - params = {} - if connection_id: - params['connectionId'] = connection_id - try: - response = AWSRetry.backoff(**retry_params)(client.describe_connections)(**params) - except (BotoCoreError, ClientError) as e: - if connection_id: - msg = "Failed to describe DirectConnect ID {0}".format(connection_id) - else: - msg = "Failed to describe DirectConnect connections" - raise DirectConnectError(msg=msg, - last_traceback=traceback.format_exc(), - exception=e) - - match = [] - connection = [] - - # look for matching connections - - if len(response.get('connections', [])) == 1 and connection_id: - if response['connections'][0]['connectionState'] != 'deleted': - match.append(response['connections'][0]['connectionId']) - connection.extend(response['connections']) - - for conn in response.get('connections', []): - if connection_name == conn['connectionName'] and conn['connectionState'] != 'deleted': - match.append(conn['connectionId']) - connection.append(conn) - - # verifying if the connections exists; if true, return connection identifier, otherwise return False - if verify and len(match) == 1: - return match[0] - elif verify: - return False - # not verifying if the connection exists; just return current connection info - elif len(connection) == 1: - return {'connection': connection[0]} - return {'connection': {}} - - -def create_connection(client, location, bandwidth, name, lag_id): - if not name: - raise DirectConnectError(msg="Failed to create a Direct Connect connection: name required.") - params = { - 'location': location, - 'bandwidth': bandwidth, - 'connectionName': name, - } - if lag_id: - params['lagId'] = lag_id - - try: - connection = AWSRetry.backoff(**retry_params)(client.create_connection)(**params) - except (BotoCoreError, ClientError) as e: - raise DirectConnectError(msg="Failed to create DirectConnect connection {0}".format(name), - last_traceback=traceback.format_exc(), - exception=e) - return connection['connectionId'] - - -def changed_properties(current_status, location, bandwidth): - current_bandwidth = current_status['bandwidth'] - current_location = current_status['location'] - - return current_bandwidth != bandwidth or current_location != location - - -@AWSRetry.backoff(**retry_params) -def update_associations(client, latest_state, connection_id, lag_id): - changed = False - if 'lagId' in latest_state and lag_id != latest_state['lagId']: - disassociate_connection_and_lag(client, connection_id, lag_id=latest_state['lagId']) - changed = True - if (changed and lag_id) or (lag_id and 'lagId' not in latest_state): - associate_connection_and_lag(client, connection_id, lag_id) - changed = True - return changed - - -def ensure_present(client, connection_id, connection_name, location, bandwidth, lag_id, forced_update): - # the connection is found; get the latest state and see if it needs to be updated - if connection_id: - latest_state = connection_status(client, connection_id=connection_id)['connection'] - if changed_properties(latest_state, location, bandwidth) and forced_update: - ensure_absent(client, connection_id) - return ensure_present(client=client, - connection_id=None, - connection_name=connection_name, - location=location, - bandwidth=bandwidth, - lag_id=lag_id, - forced_update=forced_update) - elif update_associations(client, latest_state, connection_id, lag_id): - return True, connection_id - - # no connection found; create a new one - else: - return True, create_connection(client, location, bandwidth, connection_name, lag_id) - - return False, connection_id - - -@AWSRetry.backoff(**retry_params) -def ensure_absent(client, connection_id): - changed = False - if connection_id: - delete_connection(client, connection_id) - changed = True - - return changed - - -def main(): - argument_spec = dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(), - location=dict(), - bandwidth=dict(choices=['1Gbps', '10Gbps']), - link_aggregation_group=dict(), - connection_id=dict(), - forced_update=dict(type='bool', default=False) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_one_of=[('connection_id', 'name')], - required_if=[('state', 'present', ('location', 'bandwidth'))] - ) - - connection = module.client('directconnect') - - state = module.params.get('state') - try: - connection_id = connection_exists( - connection, - connection_id=module.params.get('connection_id'), - connection_name=module.params.get('name') - ) - if not connection_id and module.params.get('connection_id'): - module.fail_json(msg="The Direct Connect connection {0} does not exist.".format(module.params.get('connection_id'))) - - if state == 'present': - changed, connection_id = ensure_present(connection, - connection_id=connection_id, - connection_name=module.params.get('name'), - location=module.params.get('location'), - bandwidth=module.params.get('bandwidth'), - lag_id=module.params.get('link_aggregation_group'), - forced_update=module.params.get('forced_update')) - response = connection_status(connection, connection_id) - elif state == 'absent': - changed = ensure_absent(connection, connection_id) - response = {} - except DirectConnectError as e: - if e.last_traceback: - module.fail_json(msg=e.msg, exception=e.last_traceback, **camel_dict_to_snake_dict(e.exception.response)) - else: - module.fail_json(msg=e.msg) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_direct_connect_gateway.py b/lib/ansible/modules/cloud/amazon/aws_direct_connect_gateway.py deleted file mode 100644 index d885368540..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_direct_connect_gateway.py +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: aws_direct_connect_gateway -author: Gobin Sougrakpam (@gobins) -version_added: "2.5" -short_description: Manage AWS Direct Connect gateway -description: - - Creates AWS Direct Connect Gateway. - - Deletes AWS Direct Connect Gateway. - - Attaches Virtual Gateways to Direct Connect Gateway. - - Detaches Virtual Gateways to Direct Connect Gateway. -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3 ] -options: - state: - description: - - Set I(state=present) to ensure a resource is created. - - Set I(state=absent) to remove a resource. - default: present - choices: [ "present", "absent"] - type: str - name: - description: - - Name of the Direct Connect Gateway to be created or deleted. - type: str - amazon_asn: - description: - - The Amazon side ASN. - - Required when I(state=present). - type: str - direct_connect_gateway_id: - description: - - The ID of an existing Direct Connect Gateway. - - Required when I(state=absent). - type: str - virtual_gateway_id: - description: - - The VPN gateway ID of an existing virtual gateway. - type: str - wait_timeout: - description: - - How long to wait for the association to be deleted. - type: int - default: 320 -''' - -EXAMPLES = ''' -- name: Create a new direct connect gateway attached to virtual private gateway - dxgw: - state: present - name: my-dx-gateway - amazon_asn: 7224 - virtual_gateway_id: vpg-12345 - register: created_dxgw - -- name: Create a new unattached dxgw - dxgw: - state: present - name: my-dx-gateway - amazon_asn: 7224 - register: created_dxgw - -''' - -RETURN = ''' -result: - description: - - The attributes of the Direct Connect Gateway - type: complex - returned: I(state=present) - contains: - amazon_side_asn: - description: ASN on the amazon side. - type: str - direct_connect_gateway_id: - description: The ID of the direct connect gateway. - type: str - direct_connect_gateway_name: - description: The name of the direct connect gateway. - type: str - direct_connect_gateway_state: - description: The state of the direct connect gateway. - type: str - owner_account: - description: The AWS account ID of the owner of the direct connect gateway. - type: str -''' - -import time -import traceback - -try: - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info, boto3_conn) -from ansible.module_utils._text import to_native - - -def dx_gateway_info(client, gateway_id, module): - try: - resp = client.describe_direct_connect_gateways( - directConnectGatewayId=gateway_id) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - if resp['directConnectGateways']: - return resp['directConnectGateways'][0] - - -def wait_for_status(client, module, gateway_id, virtual_gateway_id, status): - polling_increment_secs = 15 - max_retries = 3 - status_achieved = False - - for x in range(0, max_retries): - try: - response = check_dxgw_association( - client, - module, - gateway_id=gateway_id, - virtual_gateway_id=virtual_gateway_id) - if response['directConnectGatewayAssociations']: - if response['directConnectGatewayAssociations'][0]['associationState'] == status: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - else: - status_achieved = True - break - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return status_achieved, result - - -def associate_direct_connect_gateway(client, module, gateway_id): - params = dict() - params['virtual_gateway_id'] = module.params.get('virtual_gateway_id') - try: - response = client.create_direct_connect_gateway_association( - directConnectGatewayId=gateway_id, - virtualGatewayId=params['virtual_gateway_id']) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - status_achieved, dxgw = wait_for_status(client, module, gateway_id, params['virtual_gateway_id'], 'associating') - if not status_achieved: - module.fail_json(msg='Error waiting for dxgw to attach to vpg - please check the AWS console') - - result = response - return result - - -def delete_association(client, module, gateway_id, virtual_gateway_id): - try: - response = client.delete_direct_connect_gateway_association( - directConnectGatewayId=gateway_id, - virtualGatewayId=virtual_gateway_id) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - status_achieved, dxgw = wait_for_status(client, module, gateway_id, virtual_gateway_id, 'disassociating') - if not status_achieved: - module.fail_json(msg='Error waiting for dxgw to detach from vpg - please check the AWS console') - - result = response - return result - - -def create_dx_gateway(client, module): - params = dict() - params['name'] = module.params.get('name') - params['amazon_asn'] = module.params.get('amazon_asn') - try: - response = client.create_direct_connect_gateway( - directConnectGatewayName=params['name'], - amazonSideAsn=int(params['amazon_asn'])) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def find_dx_gateway(client, module, gateway_id=None): - params = dict() - gateways = list() - if gateway_id is not None: - params['directConnectGatewayId'] = gateway_id - while True: - try: - resp = client.describe_direct_connect_gateways(**params) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - gateways.extend(resp['directConnectGateways']) - if 'nextToken' in resp: - params['nextToken'] = resp['nextToken'] - else: - break - if gateways != []: - count = 0 - for gateway in gateways: - if module.params.get('name') == gateway['directConnectGatewayName']: - count += 1 - return gateway - return None - - -def check_dxgw_association(client, module, gateway_id, virtual_gateway_id=None): - try: - if virtual_gateway_id is None: - resp = client.describe_direct_connect_gateway_associations( - directConnectGatewayId=gateway_id - ) - else: - resp = client.describe_direct_connect_gateway_associations( - directConnectGatewayId=gateway_id, - virtualGatewayId=virtual_gateway_id, - ) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - return resp - - -def ensure_present(client, module): - # If an existing direct connect gateway matches our args - # then a match is considered to have been found and we will not create another dxgw. - - changed = False - params = dict() - result = dict() - params['name'] = module.params.get('name') - params['amazon_asn'] = module.params.get('amazon_asn') - params['virtual_gateway_id'] = module.params.get('virtual_gateway_id') - - # check if a gateway matching our module args already exists - existing_dxgw = find_dx_gateway(client, module) - - if existing_dxgw is not None and existing_dxgw['directConnectGatewayState'] != 'deleted': - gateway_id = existing_dxgw['directConnectGatewayId'] - # if a gateway_id was provided, check if it is attach to the DXGW - if params['virtual_gateway_id']: - resp = check_dxgw_association( - client, - module, - gateway_id=gateway_id, - virtual_gateway_id=params['virtual_gateway_id']) - if not resp["directConnectGatewayAssociations"]: - # attach the dxgw to the supplied virtual_gateway_id - associate_direct_connect_gateway(client, module, gateway_id) - changed = True - # if params['virtual_gateway_id'] is not provided, check the dxgw is attached to a VPG. If so, detach it. - else: - existing_dxgw = find_dx_gateway(client, module) - - resp = check_dxgw_association(client, module, gateway_id=gateway_id) - if resp["directConnectGatewayAssociations"]: - for association in resp['directConnectGatewayAssociations']: - if association['associationState'] not in ['disassociating', 'disassociated']: - delete_association( - client, - module, - gateway_id=gateway_id, - virtual_gateway_id=association['virtualGatewayId']) - else: - # create a new dxgw - new_dxgw = create_dx_gateway(client, module) - changed = True - gateway_id = new_dxgw['directConnectGateway']['directConnectGatewayId'] - - # if a vpc-id was supplied, attempt to attach it to the dxgw - if params['virtual_gateway_id']: - associate_direct_connect_gateway(client, module, gateway_id) - resp = check_dxgw_association(client, - module, - gateway_id=gateway_id - ) - if resp["directConnectGatewayAssociations"]: - changed = True - - result = dx_gateway_info(client, gateway_id, module) - return changed, result - - -def ensure_absent(client, module): - # If an existing direct connect gateway matches our args - # then a match is considered to have been found and we will not create another dxgw. - - changed = False - result = dict() - dx_gateway_id = module.params.get('direct_connect_gateway_id') - existing_dxgw = find_dx_gateway(client, module, dx_gateway_id) - if existing_dxgw is not None: - resp = check_dxgw_association(client, module, - gateway_id=dx_gateway_id) - if resp["directConnectGatewayAssociations"]: - for association in resp['directConnectGatewayAssociations']: - if association['associationState'] not in ['disassociating', 'disassociated']: - delete_association(client, module, - gateway_id=dx_gateway_id, - virtual_gateway_id=association['virtualGatewayId']) - # wait for deleting association - timeout = time.time() + module.params.get('wait_timeout') - while time.time() < timeout: - resp = check_dxgw_association(client, - module, - gateway_id=dx_gateway_id) - if resp["directConnectGatewayAssociations"] != []: - time.sleep(15) - else: - break - - try: - resp = client.delete_direct_connect_gateway( - directConnectGatewayId=dx_gateway_id - ) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - result = resp['directConnectGateway'] - return changed - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict(state=dict(default='present', choices=['present', 'absent']), - name=dict(), - amazon_asn=dict(), - virtual_gateway_id=dict(), - direct_connect_gateway_id=dict(), - wait_timeout=dict(type='int', default=320))) - required_if = [('state', 'present', ['name', 'amazon_asn']), - ('state', 'absent', ['direct_connect_gateway_id'])] - module = AnsibleModule(argument_spec=argument_spec, - required_if=required_if) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for this module') - - state = module.params.get('state') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='directconnect', region=region, endpoint=ec2_url, **aws_connect_kwargs) - - if state == 'present': - (changed, results) = ensure_present(client, module) - elif state == 'absent': - changed = ensure_absent(client, module) - results = {} - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_direct_connect_link_aggregation_group.py b/lib/ansible/modules/cloud/amazon/aws_direct_connect_link_aggregation_group.py deleted file mode 100644 index 4fd5b2b21f..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_direct_connect_link_aggregation_group.py +++ /dev/null @@ -1,470 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: aws_direct_connect_link_aggregation_group -short_description: Manage Direct Connect LAG bundles -description: - - Create, delete, or modify a Direct Connect link aggregation group. -version_added: "2.4" -author: "Sloane Hertel (@s-hertel)" -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore -options: - state: - description: - - The state of the Direct Connect link aggregation group. - choices: - - present - - absent - type: str - required: true - name: - description: - - The name of the Direct Connect link aggregation group. - type: str - link_aggregation_group_id: - description: - - The ID of the Direct Connect link aggregation group. - type: str - num_connections: - description: - - The number of connections with which to initialize the link aggregation group. - type: int - min_links: - description: - - The minimum number of physical connections that must be operational for the LAG itself to be operational. - type: int - location: - description: - - The location of the link aggregation group. - type: str - bandwidth: - description: - - The bandwidth of the link aggregation group. - type: str - force_delete: - description: - - This allows the minimum number of links to be set to 0, any hosted connections disassociated, - and any virtual interfaces associated to the LAG deleted. - type: bool - connection_id: - description: - - A connection ID to link with the link aggregation group upon creation. - type: str - delete_with_disassociation: - description: - - To be used with I(state=absent) to delete connections after disassociating them with the LAG. - type: bool - wait: - description: - - Whether or not to wait for the operation to complete. - - May be useful when waiting for virtual interfaces to be deleted. - - The time to wait can be controlled by setting I(wait_timeout). - type: bool - wait_timeout: - description: - - The duration in seconds to wait if I(wait=true). - default: 120 - type: int -""" - -EXAMPLES = """ - -# create a Direct Connect connection -- aws_direct_connect_link_aggregation_group: - state: present - location: EqDC2 - lag_id: dxlag-xxxxxxxx - bandwidth: 1Gbps - -""" - -RETURN = """ -changed: - type: str - description: Whether or not the LAG has changed. - returned: always -aws_device: - type: str - description: The AWS Direct Connection endpoint that hosts the LAG. - sample: "EqSe2-1bwfvazist2k0" - returned: when I(state=present) -connections: - type: list - description: A list of connections bundled by this LAG. - sample: - "connections": [ - { - "aws_device": "EqSe2-1bwfvazist2k0", - "bandwidth": "1Gbps", - "connection_id": "dxcon-fgzjah5a", - "connection_name": "Requested Connection 1 for Lag dxlag-fgtoh97h", - "connection_state": "down", - "lag_id": "dxlag-fgnsp4rq", - "location": "EqSe2", - "owner_account": "448830907657", - "region": "us-west-2" - } - ] - returned: when I(state=present) -connections_bandwidth: - type: str - description: The individual bandwidth of the physical connections bundled by the LAG. - sample: "1Gbps" - returned: when I(state=present) -lag_id: - type: str - description: Unique identifier for the link aggregation group. - sample: "dxlag-fgnsp4rq" - returned: when I(state=present) -lag_name: - type: str - description: User-provided name for the link aggregation group. - returned: when I(state=present) -lag_state: - type: str - description: State of the LAG. - sample: "pending" - returned: when I(state=present) -location: - type: str - description: Where the connection is located. - sample: "EqSe2" - returned: when I(state=present) -minimum_links: - type: int - description: The minimum number of physical connections that must be operational for the LAG itself to be operational. - returned: when I(state=present) -number_of_connections: - type: int - description: The number of physical connections bundled by the LAG. - returned: when I(state=present) -owner_account: - type: str - description: Owner account ID of the LAG. - returned: when I(state=present) -region: - type: str - description: The region in which the LAG exists. - returned: when I(state=present) -""" - -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, ec2_argument_spec, HAS_BOTO3, - get_aws_connection_info, boto3_conn, AWSRetry) -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.aws.direct_connect import (DirectConnectError, - delete_connection, - delete_virtual_interface, - disassociate_connection_and_lag) -import traceback -import time - -try: - import botocore -except Exception: - pass - # handled by imported HAS_BOTO3 - - -def lag_status(client, lag_id): - return lag_exists(client, lag_id=lag_id, lag_name=None, verify=False) - - -def lag_exists(client, lag_id=None, lag_name=None, verify=True): - """ If verify=True, returns the LAG ID or None - If verify=False, returns the LAG's data (or an empty dict) - """ - try: - if lag_id: - response = client.describe_lags(lagId=lag_id) - else: - response = client.describe_lags() - except botocore.exceptions.ClientError as e: - if lag_id and verify: - return False - elif lag_id: - return {} - else: - failed_op = "Failed to describe DirectConnect link aggregation groups." - raise DirectConnectError(msg=failed_op, - last_traceback=traceback.format_exc(), - exception=e) - - match = [] # List of LAG IDs that are exact matches - lag = [] # List of LAG data that are exact matches - - # look for matching connections - if len(response.get('lags', [])) == 1 and lag_id: - if response['lags'][0]['lagState'] != 'deleted': - match.append(response['lags'][0]['lagId']) - lag.append(response['lags'][0]) - else: - for each in response.get('lags', []): - if each['lagState'] != 'deleted': - if not lag_id: - if lag_name == each['lagName']: - match.append(each['lagId']) - else: - match.append(each['lagId']) - - # verifying if the connections exists; if true, return connection identifier, otherwise return False - if verify and len(match) == 1: - return match[0] - elif verify: - return False - - # not verifying if the connection exists; just return current connection info - else: - if len(lag) == 1: - return lag[0] - else: - return {} - - -def create_lag(client, num_connections, location, bandwidth, name, connection_id): - if not name: - raise DirectConnectError(msg="Failed to create a Direct Connect link aggregation group: name required.", - last_traceback=None, - exception="") - - parameters = dict(numberOfConnections=num_connections, - location=location, - connectionsBandwidth=bandwidth, - lagName=name) - if connection_id: - parameters.update(connectionId=connection_id) - try: - lag = client.create_lag(**parameters) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to create DirectConnect link aggregation group {0}".format(name), - last_traceback=traceback.format_exc(), - exception=e) - - return lag['lagId'] - - -def delete_lag(client, lag_id): - try: - client.delete_lag(lagId=lag_id) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to delete Direct Connect link aggregation group {0}.".format(lag_id), - last_traceback=traceback.format_exc(), - exception=e) - - -@AWSRetry.backoff(tries=5, delay=2, backoff=2.0, catch_extra_error_codes=['DirectConnectClientException']) -def _update_lag(client, lag_id, lag_name, min_links): - params = {} - if min_links: - params.update(minimumLinks=min_links) - if lag_name: - params.update(lagName=lag_name) - - client.update_lag(lagId=lag_id, **params) - - -def update_lag(client, lag_id, lag_name, min_links, num_connections, wait, wait_timeout): - start = time.time() - - if min_links and min_links > num_connections: - raise DirectConnectError( - msg="The number of connections {0} must be greater than the minimum number of links " - "{1} to update the LAG {2}".format(num_connections, min_links, lag_id), - last_traceback=None, - exception=None - ) - - while True: - try: - _update_lag(client, lag_id, lag_name, min_links) - except botocore.exceptions.ClientError as e: - if wait and time.time() - start <= wait_timeout: - continue - msg = "Failed to update Direct Connect link aggregation group {0}.".format(lag_id) - if "MinimumLinks cannot be set higher than the number of connections" in e.response['Error']['Message']: - msg += "Unable to set the min number of links to {0} while the LAG connections are being requested".format(min_links) - raise DirectConnectError(msg=msg, - last_traceback=traceback.format_exc(), - exception=e) - else: - break - - -def lag_changed(current_status, name, min_links): - """ Determines if a modifiable link aggregation group attribute has been modified. """ - return (name and name != current_status['lagName']) or (min_links and min_links != current_status['minimumLinks']) - - -def ensure_present(client, num_connections, lag_id, lag_name, location, bandwidth, connection_id, min_links, wait, wait_timeout): - exists = lag_exists(client, lag_id, lag_name) - if not exists and lag_id: - raise DirectConnectError(msg="The Direct Connect link aggregation group {0} does not exist.".format(lag_id), - last_traceback=None, - exception="") - - # the connection is found; get the latest state and see if it needs to be updated - if exists: - lag_id = exists - latest_state = lag_status(client, lag_id) - if lag_changed(latest_state, lag_name, min_links): - update_lag(client, lag_id, lag_name, min_links, num_connections, wait, wait_timeout) - return True, lag_id - return False, lag_id - - # no connection found; create a new one - else: - lag_id = create_lag(client, num_connections, location, bandwidth, lag_name, connection_id) - update_lag(client, lag_id, lag_name, min_links, num_connections, wait, wait_timeout) - return True, lag_id - - -def describe_virtual_interfaces(client, lag_id): - try: - response = client.describe_virtual_interfaces(connectionId=lag_id) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to describe any virtual interfaces associated with LAG: {0}".format(lag_id), - last_traceback=traceback.format_exc(), - exception=e) - return response.get('virtualInterfaces', []) - - -def get_connections_and_virtual_interfaces(client, lag_id): - virtual_interfaces = describe_virtual_interfaces(client, lag_id) - connections = lag_status(client, lag_id=lag_id).get('connections', []) - return virtual_interfaces, connections - - -def disassociate_vis(client, lag_id, virtual_interfaces): - for vi in virtual_interfaces: - delete_virtual_interface(client, vi['virtualInterfaceId']) - try: - response = client.delete_virtual_interface(virtualInterfaceId=vi['virtualInterfaceId']) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Could not delete virtual interface {0} to delete link aggregation group {1}.".format(vi, lag_id), - last_traceback=traceback.format_exc(), - exception=e) - - -def ensure_absent(client, lag_id, lag_name, force_delete, delete_with_disassociation, wait, wait_timeout): - lag_id = lag_exists(client, lag_id, lag_name) - if not lag_id: - return False - - latest_status = lag_status(client, lag_id) - - # determine the associated connections and virtual interfaces to disassociate - virtual_interfaces, connections = get_connections_and_virtual_interfaces(client, lag_id) - - # If min_links is not 0, there are associated connections, or if there are virtual interfaces, ask for force_delete - if any((latest_status['minimumLinks'], virtual_interfaces, connections)) and not force_delete: - raise DirectConnectError(msg="There are a minimum number of links, hosted connections, or associated virtual interfaces for LAG {0}. " - "To force deletion of the LAG use delete_force: True (if the LAG has virtual interfaces they will be deleted). " - "Optionally, to ensure hosted connections are deleted after disassociation use delete_with_disassociation: True " - "and wait: True (as Virtual Interfaces may take a few moments to delete)".format(lag_id), - last_traceback=None, - exception=None) - - # update min_links to be 0 so we can remove the LAG - update_lag(client, lag_id, None, 0, len(connections), wait, wait_timeout) - - # if virtual_interfaces and not delete_vi_with_disassociation: Raise failure; can't delete while vi attached - for connection in connections: - disassociate_connection_and_lag(client, connection['connectionId'], lag_id) - if delete_with_disassociation: - delete_connection(client, connection['connectionId']) - - for vi in virtual_interfaces: - delete_virtual_interface(client, vi['virtualInterfaceId']) - - start_time = time.time() - while True: - try: - delete_lag(client, lag_id) - except DirectConnectError as e: - if ('until its Virtual Interfaces are deleted' in e.exception) and (time.time() - start_time < wait_timeout) and wait: - continue - else: - return True - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(), - link_aggregation_group_id=dict(), - num_connections=dict(type='int'), - min_links=dict(type='int'), - location=dict(), - bandwidth=dict(), - connection_id=dict(), - delete_with_disassociation=dict(type='bool', default=False), - force_delete=dict(type='bool', default=False), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=120), - )) - - module = AnsibleModule(argument_spec=argument_spec, - required_one_of=[('link_aggregation_group_id', 'name')], - required_if=[('state', 'present', ('location', 'bandwidth'))]) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.") - - connection = boto3_conn(module, conn_type='client', - resource='directconnect', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - - state = module.params.get('state') - response = {} - try: - if state == 'present': - changed, lag_id = ensure_present(connection, - num_connections=module.params.get("num_connections"), - lag_id=module.params.get("link_aggregation_group_id"), - lag_name=module.params.get("name"), - location=module.params.get("location"), - bandwidth=module.params.get("bandwidth"), - connection_id=module.params.get("connection_id"), - min_links=module.params.get("min_links"), - wait=module.params.get("wait"), - wait_timeout=module.params.get("wait_timeout")) - response = lag_status(connection, lag_id) - elif state == "absent": - changed = ensure_absent(connection, - lag_id=module.params.get("link_aggregation_group_id"), - lag_name=module.params.get("name"), - force_delete=module.params.get("force_delete"), - delete_with_disassociation=module.params.get("delete_with_disassociation"), - wait=module.params.get('wait'), - wait_timeout=module.params.get('wait_timeout')) - except DirectConnectError as e: - if e.last_traceback: - module.fail_json(msg=e.msg, exception=e.last_traceback, **camel_dict_to_snake_dict(e.exception)) - else: - module.fail_json(msg=e.msg) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_direct_connect_virtual_interface.py b/lib/ansible/modules/cloud/amazon/aws_direct_connect_virtual_interface.py deleted file mode 100644 index b73685d763..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_direct_connect_virtual_interface.py +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_direct_connect_virtual_interface -short_description: Manage Direct Connect virtual interfaces -description: - - Create, delete, or modify a Direct Connect public or private virtual interface. -version_added: "2.5" -author: "Sloane Hertel (@s-hertel)" -requirements: - - boto3 - - botocore -options: - state: - description: - - The desired state of the Direct Connect virtual interface. - choices: [present, absent] - type: str - required: true - id_to_associate: - description: - - The ID of the link aggregation group or connection to associate with the virtual interface. - aliases: [link_aggregation_group_id, connection_id] - type: str - required: true - public: - description: - - The type of virtual interface. - type: bool - name: - description: - - The name of the virtual interface. - type: str - vlan: - description: - - The VLAN ID. - default: 100 - type: int - bgp_asn: - description: - - The autonomous system (AS) number for Border Gateway Protocol (BGP) configuration. - default: 65000 - type: int - authentication_key: - description: - - The authentication key for BGP configuration. - type: str - amazon_address: - description: - - The amazon address CIDR with which to create the virtual interface. - type: str - customer_address: - description: - - The customer address CIDR with which to create the virtual interface. - type: str - address_type: - description: - - The type of IP address for the BGP peer. - type: str - cidr: - description: - - A list of route filter prefix CIDRs with which to create the public virtual interface. - type: list - elements: str - virtual_gateway_id: - description: - - The virtual gateway ID required for creating a private virtual interface. - type: str - virtual_interface_id: - description: - - The virtual interface ID. - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -RETURN = ''' -address_family: - description: The address family for the BGP peer. - returned: always - type: str - sample: ipv4 -amazon_address: - description: IP address assigned to the Amazon interface. - returned: always - type: str - sample: 169.254.255.1/30 -asn: - description: The autonomous system (AS) number for Border Gateway Protocol (BGP) configuration. - returned: always - type: int - sample: 65000 -auth_key: - description: The authentication key for BGP configuration. - returned: always - type: str - sample: 0xZ59Y1JZ2oDOSh6YriIlyRE -bgp_peers: - description: A list of the BGP peers configured on this virtual interface. - returned: always - type: complex - contains: - address_family: - description: The address family for the BGP peer. - returned: always - type: str - sample: ipv4 - amazon_address: - description: IP address assigned to the Amazon interface. - returned: always - type: str - sample: 169.254.255.1/30 - asn: - description: The autonomous system (AS) number for Border Gateway Protocol (BGP) configuration. - returned: always - type: int - sample: 65000 - auth_key: - description: The authentication key for BGP configuration. - returned: always - type: str - sample: 0xZ59Y1JZ2oDOSh6YriIlyRE - bgp_peer_state: - description: The state of the BGP peer (verifying, pending, available) - returned: always - type: str - sample: available - bgp_status: - description: The up/down state of the BGP peer. - returned: always - type: str - sample: up - customer_address: - description: IP address assigned to the customer interface. - returned: always - type: str - sample: 169.254.255.2/30 -changed: - description: Indicated if the virtual interface has been created/modified/deleted - returned: always - type: bool - sample: false -connection_id: - description: - - The ID of the connection. This field is also used as the ID type for operations that - use multiple connection types (LAG, interconnect, and/or connection). - returned: always - type: str - sample: dxcon-fgb175av -customer_address: - description: IP address assigned to the customer interface. - returned: always - type: str - sample: 169.254.255.2/30 -customer_router_config: - description: Information for generating the customer router configuration. - returned: always - type: str -location: - description: Where the connection is located. - returned: always - type: str - sample: EqDC2 -owner_account: - description: The AWS account that will own the new virtual interface. - returned: always - type: str - sample: '123456789012' -route_filter_prefixes: - description: A list of routes to be advertised to the AWS network in this region (public virtual interface). - returned: always - type: complex - contains: - cidr: - description: A routes to be advertised to the AWS network in this region. - returned: always - type: str - sample: 54.227.92.216/30 -virtual_gateway_id: - description: The ID of the virtual private gateway to a VPC. This only applies to private virtual interfaces. - returned: when I(public=False) - type: str - sample: vgw-f3ce259a -virtual_interface_id: - description: The ID of the virtual interface. - returned: always - type: str - sample: dxvif-fh0w7cex -virtual_interface_name: - description: The name of the virtual interface assigned by the customer. - returned: always - type: str - sample: test_virtual_interface -virtual_interface_state: - description: State of the virtual interface (confirming, verifying, pending, available, down, rejected). - returned: always - type: str - sample: available -virtual_interface_type: - description: The type of virtual interface (private, public). - returned: always - type: str - sample: private -vlan: - description: The VLAN ID. - returned: always - type: int - sample: 100 -''' - -EXAMPLES = ''' ---- -- name: create an association between a LAG and connection - aws_direct_connect_virtual_interface: - state: present - name: "{{ name }}" - link_aggregation_group_id: LAG-XXXXXXXX - connection_id: dxcon-XXXXXXXX - -- name: remove an association between a connection and virtual interface - aws_direct_connect_virtual_interface: - state: absent - connection_id: dxcon-XXXXXXXX - virtual_interface_id: dxv-XXXXXXXX - -''' - -import traceback -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.direct_connect import DirectConnectError, delete_virtual_interface -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - # handled by AnsibleAWSModule - pass - - -def try_except_ClientError(failure_msg): - ''' - Wrapper for boto3 calls that uses AWSRetry and handles exceptions - ''' - def wrapper(f): - def run_func(*args, **kwargs): - try: - result = AWSRetry.backoff(tries=8, delay=5, catch_extra_error_codes=['DirectConnectClientException'])(f)(*args, **kwargs) - except (ClientError, BotoCoreError) as e: - raise DirectConnectError(failure_msg, traceback.format_exc(), e) - return result - return run_func - return wrapper - - -def find_unique_vi(client, connection_id, virtual_interface_id, name): - ''' - Determines if the virtual interface exists. Returns the virtual interface ID if an exact match is found. - If multiple matches are found False is returned. If no matches are found None is returned. - ''' - - # Get the virtual interfaces, filtering by the ID if provided. - vi_params = {} - if virtual_interface_id: - vi_params = {'virtualInterfaceId': virtual_interface_id} - - virtual_interfaces = try_except_ClientError( - failure_msg="Failed to describe virtual interface")( - client.describe_virtual_interfaces)(**vi_params).get('virtualInterfaces') - - # Remove deleting/deleted matches from the results. - virtual_interfaces = [vi for vi in virtual_interfaces if vi['virtualInterfaceState'] not in ('deleting', 'deleted')] - - matching_virtual_interfaces = filter_virtual_interfaces(virtual_interfaces, name, connection_id) - return exact_match(matching_virtual_interfaces) - - -def exact_match(virtual_interfaces): - ''' - Returns the virtual interface ID if one was found, - None if the virtual interface ID needs to be created, - False if an exact match was not found - ''' - - if not virtual_interfaces: - return None - if len(virtual_interfaces) == 1: - return virtual_interfaces[0]['virtualInterfaceId'] - else: - return False - - -def filter_virtual_interfaces(virtual_interfaces, name, connection_id): - ''' - Filters the available virtual interfaces to try to find a unique match - ''' - # Filter by name if provided. - if name: - matching_by_name = find_virtual_interface_by_name(virtual_interfaces, name) - if len(matching_by_name) == 1: - return matching_by_name - else: - matching_by_name = virtual_interfaces - - # If there isn't a unique match filter by connection ID as last resort (because connection_id may be a connection yet to be associated) - if connection_id and len(matching_by_name) > 1: - matching_by_connection_id = find_virtual_interface_by_connection_id(matching_by_name, connection_id) - if len(matching_by_connection_id) == 1: - return matching_by_connection_id - else: - matching_by_connection_id = matching_by_name - - return matching_by_connection_id - - -def find_virtual_interface_by_connection_id(virtual_interfaces, connection_id): - ''' - Return virtual interfaces that have the connection_id associated - ''' - return [vi for vi in virtual_interfaces if vi['connectionId'] == connection_id] - - -def find_virtual_interface_by_name(virtual_interfaces, name): - ''' - Return virtual interfaces that match the provided name - ''' - return [vi for vi in virtual_interfaces if vi['virtualInterfaceName'] == name] - - -def vi_state(client, virtual_interface_id): - ''' - Returns the state of the virtual interface. - ''' - err_msg = "Failed to describe virtual interface: {0}".format(virtual_interface_id) - vi = try_except_ClientError(failure_msg=err_msg)(client.describe_virtual_interfaces)(virtualInterfaceId=virtual_interface_id) - return vi['virtualInterfaces'][0] - - -def assemble_params_for_creating_vi(params): - ''' - Returns kwargs to use in the call to create the virtual interface - - Params for public virtual interfaces: - virtualInterfaceName, vlan, asn, authKey, amazonAddress, customerAddress, addressFamily, cidr - Params for private virtual interfaces: - virtualInterfaceName, vlan, asn, authKey, amazonAddress, customerAddress, addressFamily, virtualGatewayId - ''' - - public = params['public'] - name = params['name'] - vlan = params['vlan'] - bgp_asn = params['bgp_asn'] - auth_key = params['authentication_key'] - amazon_addr = params['amazon_address'] - customer_addr = params['customer_address'] - family_addr = params['address_type'] - cidr = params['cidr'] - virtual_gateway_id = params['virtual_gateway_id'] - - parameters = dict(virtualInterfaceName=name, vlan=vlan, asn=bgp_asn) - opt_params = dict(authKey=auth_key, amazonAddress=amazon_addr, customerAddress=customer_addr, addressFamily=family_addr) - - for name, value in opt_params.items(): - if value: - parameters[name] = value - - # virtual interface type specific parameters - if public and cidr: - parameters['routeFilterPrefixes'] = [{'cidr': c} for c in cidr] - if not public: - parameters['virtualGatewayId'] = virtual_gateway_id - - return parameters - - -def create_vi(client, public, associated_id, creation_params): - ''' - :param public: a boolean - :param associated_id: a link aggregation group ID or connection ID to associate - with the virtual interface. - :param creation_params: a dict of parameters to use in the boto call - :return The ID of the created virtual interface - ''' - err_msg = "Failed to create virtual interface" - if public: - vi = try_except_ClientError(failure_msg=err_msg)(client.create_public_virtual_interface)(connectionId=associated_id, - newPublicVirtualInterface=creation_params) - else: - vi = try_except_ClientError(failure_msg=err_msg)(client.create_private_virtual_interface)(connectionId=associated_id, - newPrivateVirtualInterface=creation_params) - return vi['virtualInterfaceId'] - - -def modify_vi(client, virtual_interface_id, connection_id): - ''' - Associate a new connection ID - ''' - err_msg = "Unable to associate {0} with virtual interface {1}".format(connection_id, virtual_interface_id) - try_except_ClientError(failure_msg=err_msg)(client.associate_virtual_interface)(virtualInterfaceId=virtual_interface_id, - connectionId=connection_id) - - -def needs_modification(client, virtual_interface_id, connection_id): - ''' - Determine if the associated connection ID needs to be updated - ''' - return vi_state(client, virtual_interface_id).get('connectionId') != connection_id - - -def ensure_state(connection, module): - changed = False - - state = module.params['state'] - connection_id = module.params['id_to_associate'] - public = module.params['public'] - name = module.params['name'] - - virtual_interface_id = find_unique_vi(connection, connection_id, module.params.get('virtual_interface_id'), name) - - if virtual_interface_id is False: - module.fail_json(msg="Multiple virtual interfaces were found. Use the virtual_interface_id, name, " - "and connection_id options if applicable to find a unique match.") - - if state == 'present': - - if not virtual_interface_id and module.params['virtual_interface_id']: - module.fail_json(msg="The virtual interface {0} does not exist.".format(module.params['virtual_interface_id'])) - - elif not virtual_interface_id: - assembled_params = assemble_params_for_creating_vi(module.params) - virtual_interface_id = create_vi(connection, public, connection_id, assembled_params) - changed = True - - if needs_modification(connection, virtual_interface_id, connection_id): - modify_vi(connection, virtual_interface_id, connection_id) - changed = True - - latest_state = vi_state(connection, virtual_interface_id) - - else: - if virtual_interface_id: - delete_virtual_interface(connection, virtual_interface_id) - changed = True - - latest_state = {} - - return changed, latest_state - - -def main(): - argument_spec = dict( - state=dict(required=True, choices=['present', 'absent']), - id_to_associate=dict(required=True, aliases=['link_aggregation_group_id', 'connection_id']), - public=dict(type='bool'), - name=dict(), - vlan=dict(type='int', default=100), - bgp_asn=dict(type='int', default=65000), - authentication_key=dict(), - amazon_address=dict(), - customer_address=dict(), - address_type=dict(), - cidr=dict(type='list'), - virtual_gateway_id=dict(), - virtual_interface_id=dict() - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_one_of=[['virtual_interface_id', 'name']], - required_if=[['state', 'present', ['public']], - ['public', False, ['virtual_gateway_id']], - ['public', True, ['amazon_address']], - ['public', True, ['customer_address']], - ['public', True, ['cidr']]]) - - connection = module.client('directconnect') - - try: - changed, latest_state = ensure_state(connection, module) - except DirectConnectError as e: - if e.exception: - module.fail_json_aws(exception=e.exception, msg=e.msg) - else: - module.fail_json(msg=e.msg) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(latest_state)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py b/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py deleted file mode 100644 index ae0f9d1a0f..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_eks_cluster -short_description: Manage Elastic Kubernetes Service Clusters -description: - - Manage Elastic Kubernetes Service Clusters -version_added: "2.7" - -author: Will Thames (@willthames) - -options: - name: - description: Name of EKS cluster - required: True - type: str - version: - description: Kubernetes version - defaults to latest - type: str - role_arn: - description: ARN of IAM role used by the EKS cluster - type: str - subnets: - description: list of subnet IDs for the Kubernetes cluster - type: list - elements: str - security_groups: - description: list of security group names or IDs - type: list - elements: str - state: - description: desired state of the EKS cluster - choices: - - absent - - present - default: present - type: str - wait: - description: >- - Specifies whether the module waits until the cluster is active or deleted - before moving on. It takes "usually less than 10 minutes" per AWS documentation. - type: bool - default: false - wait_timeout: - description: >- - The duration in seconds to wait for the cluster to become active. Defaults - to 1200 seconds (20 minutes). - default: 1200 - type: int - -requirements: [ 'botocore', 'boto3' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Create an EKS cluster - aws_eks_cluster: - name: my_cluster - version: 1.14 - role_arn: my_eks_role - subnets: - - subnet-aaaa1111 - security_groups: - - my_eks_sg - - sg-abcd1234 - register: caller_facts - -- name: Remove an EKS cluster - aws_eks_cluster: - name: my_cluster - wait: yes - state: absent -''' - -RETURN = ''' -arn: - description: ARN of the EKS cluster - returned: when state is present - type: str - sample: arn:aws:eks:us-west-2:111111111111:cluster/my-eks-cluster -certificate_authority: - description: Dictionary containing Certificate Authority Data for cluster - returned: after creation - type: complex - contains: - data: - description: Base-64 encoded Certificate Authority Data for cluster - returned: when the cluster has been created and is active - type: str -endpoint: - description: Kubernetes API server endpoint - returned: when the cluster has been created and is active - type: str - sample: https://API_SERVER_ENDPOINT.yl4.us-west-2.eks.amazonaws.com -created_at: - description: Cluster creation date and time - returned: when state is present - type: str - sample: '2018-06-06T11:56:56.242000+00:00' -name: - description: EKS cluster name - returned: when state is present - type: str - sample: my-eks-cluster -resources_vpc_config: - description: VPC configuration of the cluster - returned: when state is present - type: complex - contains: - security_group_ids: - description: List of security group IDs - returned: always - type: list - sample: - - sg-abcd1234 - - sg-aaaa1111 - subnet_ids: - description: List of subnet IDs - returned: always - type: list - sample: - - subnet-abcdef12 - - subnet-345678ab - - subnet-cdef1234 - vpc_id: - description: VPC id - returned: always - type: str - sample: vpc-a1b2c3d4 -role_arn: - description: ARN of the IAM role used by the cluster - returned: when state is present - type: str - sample: arn:aws:iam::111111111111:role/aws_eks_cluster_role -status: - description: status of the EKS cluster - returned: when state is present - type: str - sample: - - CREATING - - ACTIVE -version: - description: Kubernetes version of the cluster - returned: when state is present - type: str - sample: '1.10' -''' - - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names -from ansible.module_utils.aws.waiters import get_waiter - -try: - import botocore.exceptions -except ImportError: - pass # caught by AnsibleAWSModule - - -def ensure_present(client, module): - name = module.params.get('name') - subnets = module.params['subnets'] - groups = module.params['security_groups'] - wait = module.params.get('wait') - cluster = get_cluster(client, module) - try: - ec2 = module.client('ec2') - vpc_id = ec2.describe_subnets(SubnetIds=[subnets[0]])['Subnets'][0]['VpcId'] - groups = get_ec2_security_group_ids_from_names(groups, ec2, vpc_id) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't lookup security groups") - - if cluster: - if set(cluster['resourcesVpcConfig']['subnetIds']) != set(subnets): - module.fail_json(msg="Cannot modify subnets of existing cluster") - if set(cluster['resourcesVpcConfig']['securityGroupIds']) != set(groups): - module.fail_json(msg="Cannot modify security groups of existing cluster") - if module.params.get('version') and module.params.get('version') != cluster['version']: - module.fail_json(msg="Cannot modify version of existing cluster") - - if wait: - wait_until(client, module, 'cluster_active') - # Ensure that fields that are only available for active clusters are - # included in the returned value - cluster = get_cluster(client, module) - - module.exit_json(changed=False, **camel_dict_to_snake_dict(cluster)) - - if module.check_mode: - module.exit_json(changed=True) - try: - params = dict(name=name, - roleArn=module.params['role_arn'], - resourcesVpcConfig=dict( - subnetIds=subnets, - securityGroupIds=groups), - clientRequestToken='ansible-create-%s' % name) - if module.params['version']: - params['version'] = module.params['version'] - cluster = client.create_cluster(**params)['cluster'] - except botocore.exceptions.EndpointConnectionError as e: - module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't create cluster %s" % name) - - if wait: - wait_until(client, module, 'cluster_active') - # Ensure that fields that are only available for active clusters are - # included in the returned value - cluster = get_cluster(client, module) - - module.exit_json(changed=True, **camel_dict_to_snake_dict(cluster)) - - -def ensure_absent(client, module): - name = module.params.get('name') - existing = get_cluster(client, module) - wait = module.params.get('wait') - if not existing: - module.exit_json(changed=False) - if not module.check_mode: - try: - client.delete_cluster(name=module.params['name']) - except botocore.exceptions.EndpointConnectionError as e: - module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't delete cluster %s" % name) - - if wait: - wait_until(client, module, 'cluster_deleted') - - module.exit_json(changed=True) - - -def get_cluster(client, module): - name = module.params.get('name') - try: - return client.describe_cluster(name=name)['cluster'] - except is_boto3_error_code('ResourceNotFoundException'): - return None - except botocore.exceptions.EndpointConnectionError as e: # pylint: disable=duplicate-except - module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Couldn't get cluster %s" % name) - - -def wait_until(client, module, waiter_name='cluster_active'): - name = module.params.get('name') - wait_timeout = module.params.get('wait_timeout') - - waiter = get_waiter(client, waiter_name) - attempts = 1 + int(wait_timeout / waiter.config.delay) - waiter.wait(name=name, WaiterConfig={'MaxAttempts': attempts}) - - -def main(): - argument_spec = dict( - name=dict(required=True), - version=dict(), - role_arn=dict(), - subnets=dict(type='list'), - security_groups=dict(type='list'), - state=dict(choices=['absent', 'present'], default='present'), - wait=dict(default=False, type='bool'), - wait_timeout=dict(default=1200, type='int') - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_if=[['state', 'present', ['role_arn', 'subnets', 'security_groups']]], - supports_check_mode=True, - ) - - if not module.botocore_at_least("1.10.32"): - module.fail_json(msg='aws_eks_cluster module requires botocore >= 1.10.32') - - if (not module.botocore_at_least("1.12.38") and - module.params.get('state') == 'absent' and - module.params.get('wait')): - module.fail_json(msg='aws_eks_cluster: wait=yes when state=absent requires botocore >= 1.12.38') - - client = module.client('eks') - - if module.params.get('state') == 'present': - ensure_present(client, module) - else: - ensure_absent(client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py b/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py deleted file mode 100644 index ec22f2c873..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' - } - -DOCUMENTATION = ''' ---- -module: aws_elasticbeanstalk_app - -short_description: Create, update, and delete an elastic beanstalk application - -version_added: "2.5" - -description: - - Creates, updates, deletes beanstalk applications if app_name is provided. - -options: - app_name: - description: - - Name of the beanstalk application you wish to manage. - aliases: [ 'name' ] - type: str - description: - description: - - The description of the application. - type: str - state: - description: - - Whether to ensure the application is present or absent. - default: present - choices: ['absent','present'] - type: str - terminate_by_force: - description: - - When I(terminate_by_force=true), running environments will be terminated before deleting the application. - default: false - type: bool -author: - - Harpreet Singh (@hsingh) - - Stephen Granger (@viper233) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create or update an application -- aws_elasticbeanstalk_app: - app_name: Sample_App - description: "Hello World App" - state: present - -# Delete application -- aws_elasticbeanstalk_app: - app_name: Sample_App - state: absent - -''' - -RETURN = ''' -app: - description: Beanstalk application. - returned: always - type: dict - sample: { - "ApplicationName": "app-name", - "ConfigurationTemplates": [], - "DateCreated": "2016-12-28T14:50:03.185000+00:00", - "DateUpdated": "2016-12-28T14:50:03.185000+00:00", - "Description": "description", - "Versions": [ - "1.0.0", - "1.0.1" - ] - } -output: - description: Message indicating what change will occur. - returned: in check mode - type: str - sample: App is up-to-date -''' - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule - - -def describe_app(ebs, app_name, module): - apps = list_apps(ebs, app_name, module) - - return None if len(apps) != 1 else apps[0] - - -def list_apps(ebs, app_name, module): - try: - if app_name is not None: - apps = ebs.describe_applications(ApplicationNames=[app_name]) - else: - apps = ebs.describe_applications() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not describe application") - - return apps.get("Applications", []) - - -def check_app(ebs, app, module): - app_name = module.params['app_name'] - description = module.params['description'] - state = module.params['state'] - terminate_by_force = module.params['terminate_by_force'] - - result = {} - - if state == 'present' and app is None: - result = dict(changed=True, output="App would be created") - elif state == 'present' and app.get("Description", None) != description: - result = dict(changed=True, output="App would be updated", app=app) - elif state == 'present' and app.get("Description", None) == description: - result = dict(changed=False, output="App is up-to-date", app=app) - elif state == 'absent' and app is None: - result = dict(changed=False, output="App does not exist", app={}) - elif state == 'absent' and app is not None: - result = dict(changed=True, output="App will be deleted", app=app) - elif state == 'absent' and app is not None and terminate_by_force is True: - result = dict(changed=True, output="Running environments terminated before the App will be deleted", app=app) - - module.exit_json(**result) - - -def filter_empty(**kwargs): - retval = {} - for k, v in kwargs.items(): - if v: - retval[k] = v - return retval - - -def main(): - argument_spec = dict( - app_name=dict(aliases=['name'], type='str', required=False), - description=dict(), - state=dict(choices=['present', 'absent'], default='present'), - terminate_by_force=dict(type='bool', default=False, required=False) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - app_name = module.params['app_name'] - description = module.params['description'] - state = module.params['state'] - terminate_by_force = module.params['terminate_by_force'] - - if app_name is None: - module.fail_json(msg='Module parameter "app_name" is required') - - result = {} - - ebs = module.client('elasticbeanstalk') - - app = describe_app(ebs, app_name, module) - - if module.check_mode: - check_app(ebs, app, module) - module.fail_json(msg='ASSERTION FAILURE: check_app() should not return control.') - - if state == 'present': - if app is None: - try: - create_app = ebs.create_application(**filter_empty(ApplicationName=app_name, - Description=description)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not create application") - - app = describe_app(ebs, app_name, module) - - result = dict(changed=True, app=app) - else: - if app.get("Description", None) != description: - try: - if not description: - ebs.update_application(ApplicationName=app_name) - else: - ebs.update_application(ApplicationName=app_name, Description=description) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not update application") - - app = describe_app(ebs, app_name, module) - - result = dict(changed=True, app=app) - else: - result = dict(changed=False, app=app) - - else: - if app is None: - result = dict(changed=False, output='Application not found', app={}) - else: - try: - if terminate_by_force: - # Running environments will be terminated before deleting the application - ebs.delete_application(ApplicationName=app_name, TerminateEnvByForce=terminate_by_force) - else: - ebs.delete_application(ApplicationName=app_name) - changed = True - except BotoCoreError as e: - module.fail_json_aws(e, msg="Cannot terminate app") - except ClientError as e: - if 'It is currently pending deletion.' not in e.response['Error']['Message']: - module.fail_json_aws(e, msg="Cannot terminate app") - else: - changed = False - - result = dict(changed=changed, app=app) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_glue_connection.py b/lib/ansible/modules/cloud/amazon/aws_glue_connection.py deleted file mode 100644 index 4909abb240..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_glue_connection.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2018, Rob White (@wimnat) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_glue_connection -short_description: Manage an AWS Glue connection -description: - - Manage an AWS Glue connection. See U(https://aws.amazon.com/glue/) for details. -version_added: "2.6" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - catalog_id: - description: - - The ID of the Data Catalog in which to create the connection. If none is supplied, - the AWS account ID is used by default. - type: str - connection_properties: - description: - - A dict of key-value pairs used as parameters for this connection. - - Required when I(state=present). - type: dict - connection_type: - description: - - The type of the connection. Currently, only JDBC is supported; SFTP is not supported. - default: JDBC - choices: [ 'JDBC', 'SFTP' ] - type: str - description: - description: - - The description of the connection. - type: str - match_criteria: - description: - - A list of UTF-8 strings that specify the criteria that you can use in selecting this connection. - type: list - elements: str - name: - description: - - The name of the connection. - required: true - type: str - security_groups: - description: - - A list of security groups to be used by the connection. Use either security group name or ID. - type: list - elements: str - state: - description: - - Create or delete the AWS Glue connection. - required: true - choices: [ 'present', 'absent' ] - type: str - subnet_id: - description: - - The subnet ID used by the connection. - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an AWS Glue connection -- aws_glue_connection: - name: my-glue-connection - connection_properties: - JDBC_CONNECTION_URL: jdbc:mysql://mydb:3306/databasename - USERNAME: my-username - PASSWORD: my-password - state: present - -# Delete an AWS Glue connection -- aws_glue_connection: - name: my-glue-connection - state: absent - -''' - -RETURN = ''' -connection_properties: - description: A dict of key-value pairs used as parameters for this connection. - returned: when state is present - type: dict - sample: {'JDBC_CONNECTION_URL':'jdbc:mysql://mydb:3306/databasename','USERNAME':'x','PASSWORD':'y'} -connection_type: - description: The type of the connection. - returned: when state is present - type: str - sample: JDBC -creation_time: - description: The time this connection definition was created. - returned: when state is present - type: str - sample: "2018-04-21T05:19:58.326000+00:00" -description: - description: Description of the job being defined. - returned: when state is present - type: str - sample: My first Glue job -last_updated_time: - description: The last time this connection definition was updated. - returned: when state is present - type: str - sample: "2018-04-21T05:19:58.326000+00:00" -match_criteria: - description: A list of criteria that can be used in selecting this connection. - returned: when state is present - type: list - sample: [] -name: - description: The name of the connection definition. - returned: when state is present - type: str - sample: my-glue-connection -physical_connection_requirements: - description: A dict of physical connection requirements, such as VPC and SecurityGroup, - needed for making this connection successfully. - returned: when state is present - type: dict - sample: {'subnet-id':'subnet-aabbccddee'} -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names - -# Non-ansible imports -import copy -import time -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass - - -def _get_glue_connection(connection, module): - """ - Get an AWS Glue connection based on name. If not found, return None. - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :return: boto3 Glue connection dict or None if not found - """ - - connection_name = module.params.get("name") - connection_catalog_id = module.params.get("catalog_id") - - params = {'Name': connection_name} - if connection_catalog_id is not None: - params['CatalogId'] = connection_catalog_id - - try: - return connection.get_connection(**params)['Connection'] - except (BotoCoreError, ClientError) as e: - if e.response['Error']['Code'] == 'EntityNotFoundException': - return None - else: - raise e - - -def _compare_glue_connection_params(user_params, current_params): - """ - Compare Glue connection params. If there is a difference, return True immediately else return False - - :param user_params: the Glue connection parameters passed by the user - :param current_params: the Glue connection parameters currently configured - :return: True if any parameter is mismatched else False - """ - - # Weirdly, boto3 doesn't return some keys if the value is empty e.g. Description - # To counter this, add the key if it's missing with a blank value - - if 'Description' not in current_params: - current_params['Description'] = "" - if 'MatchCriteria' not in current_params: - current_params['MatchCriteria'] = list() - if 'PhysicalConnectionRequirements' not in current_params: - current_params['PhysicalConnectionRequirements'] = dict() - current_params['PhysicalConnectionRequirements']['SecurityGroupIdList'] = [] - current_params['PhysicalConnectionRequirements']['SubnetId'] = "" - - if 'ConnectionProperties' in user_params['ConnectionInput'] and user_params['ConnectionInput']['ConnectionProperties'] \ - != current_params['ConnectionProperties']: - return True - if 'ConnectionType' in user_params['ConnectionInput'] and user_params['ConnectionInput']['ConnectionType'] \ - != current_params['ConnectionType']: - return True - if 'Description' in user_params['ConnectionInput'] and user_params['ConnectionInput']['Description'] != current_params['Description']: - return True - if 'MatchCriteria' in user_params['ConnectionInput'] and set(user_params['ConnectionInput']['MatchCriteria']) != set(current_params['MatchCriteria']): - return True - if 'PhysicalConnectionRequirements' in user_params['ConnectionInput']: - if 'SecurityGroupIdList' in user_params['ConnectionInput']['PhysicalConnectionRequirements'] and \ - set(user_params['ConnectionInput']['PhysicalConnectionRequirements']['SecurityGroupIdList']) \ - != set(current_params['PhysicalConnectionRequirements']['SecurityGroupIdList']): - return True - if 'SubnetId' in user_params['ConnectionInput']['PhysicalConnectionRequirements'] and \ - user_params['ConnectionInput']['PhysicalConnectionRequirements']['SubnetId'] \ - != current_params['PhysicalConnectionRequirements']['SubnetId']: - return True - - return False - - -def create_or_update_glue_connection(connection, connection_ec2, module, glue_connection): - """ - Create or update an AWS Glue connection - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :param glue_connection: a dict of AWS Glue connection parameters or None - :return: - """ - - changed = False - params = dict() - params['ConnectionInput'] = dict() - params['ConnectionInput']['Name'] = module.params.get("name") - params['ConnectionInput']['ConnectionType'] = module.params.get("connection_type") - params['ConnectionInput']['ConnectionProperties'] = module.params.get("connection_properties") - if module.params.get("catalog_id") is not None: - params['CatalogId'] = module.params.get("catalog_id") - if module.params.get("description") is not None: - params['ConnectionInput']['Description'] = module.params.get("description") - if module.params.get("match_criteria") is not None: - params['ConnectionInput']['MatchCriteria'] = module.params.get("match_criteria") - if module.params.get("security_groups") is not None or module.params.get("subnet_id") is not None: - params['ConnectionInput']['PhysicalConnectionRequirements'] = dict() - if module.params.get("security_groups") is not None: - # Get security group IDs from names - security_group_ids = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection_ec2, boto3=True) - params['ConnectionInput']['PhysicalConnectionRequirements']['SecurityGroupIdList'] = security_group_ids - if module.params.get("subnet_id") is not None: - params['ConnectionInput']['PhysicalConnectionRequirements']['SubnetId'] = module.params.get("subnet_id") - - # If glue_connection is not None then check if it needs to be modified, else create it - if glue_connection: - if _compare_glue_connection_params(params, glue_connection): - try: - # We need to slightly modify the params for an update - update_params = copy.deepcopy(params) - update_params['Name'] = update_params['ConnectionInput']['Name'] - connection.update_connection(**update_params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - else: - try: - connection.create_connection(**params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - # If changed, get the Glue connection again - if changed: - glue_connection = None - for i in range(10): - glue_connection = _get_glue_connection(connection, module) - if glue_connection is not None: - break - time.sleep(10) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(glue_connection)) - - -def delete_glue_connection(connection, module, glue_connection): - """ - Delete an AWS Glue connection - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :param glue_connection: a dict of AWS Glue connection parameters or None - :return: - """ - - changed = False - - params = {'ConnectionName': module.params.get("name")} - if module.params.get("catalog_id") is not None: - params['CatalogId'] = module.params.get("catalog_id") - - if glue_connection: - try: - connection.delete_connection(**params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - module.exit_json(changed=changed) - - -def main(): - - argument_spec = ( - dict( - catalog_id=dict(type='str'), - connection_properties=dict(type='dict'), - connection_type=dict(type='str', default='JDBC', choices=['JDBC', 'SFTP']), - description=dict(type='str'), - match_criteria=dict(type='list'), - name=dict(required=True, type='str'), - security_groups=dict(type='list'), - state=dict(required=True, choices=['present', 'absent'], type='str'), - subnet_id=dict(type='str') - ) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[ - ('state', 'present', ['connection_properties']) - ] - ) - - connection_glue = module.client('glue') - connection_ec2 = module.client('ec2') - - glue_connection = _get_glue_connection(connection_glue, module) - - if module.params.get("state") == 'present': - create_or_update_glue_connection(connection_glue, connection_ec2, module, glue_connection) - else: - delete_glue_connection(connection_glue, module, glue_connection) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_glue_job.py b/lib/ansible/modules/cloud/amazon/aws_glue_job.py deleted file mode 100644 index 9fa61ac1a4..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_glue_job.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2018, Rob White (@wimnat) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_glue_job -short_description: Manage an AWS Glue job -description: - - Manage an AWS Glue job. See U(https://aws.amazon.com/glue/) for details. -version_added: "2.6" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - allocated_capacity: - description: - - The number of AWS Glue data processing units (DPUs) to allocate to this Job. From 2 to 100 DPUs - can be allocated; the default is 10. A DPU is a relative measure of processing power that consists - of 4 vCPUs of compute capacity and 16 GB of memory. - type: int - command_name: - description: - - The name of the job command. This must be 'glueetl'. - default: glueetl - type: str - command_script_location: - description: - - The S3 path to a script that executes a job. - - Required when I(state=present). - type: str - connections: - description: - - A list of Glue connections used for this job. - type: list - elements: str - default_arguments: - description: - - A dict of default arguments for this job. You can specify arguments here that your own job-execution - script consumes, as well as arguments that AWS Glue itself consumes. - type: dict - description: - description: - - Description of the job being defined. - type: str - max_concurrent_runs: - description: - - The maximum number of concurrent runs allowed for the job. The default is 1. An error is returned when - this threshold is reached. The maximum value you can specify is controlled by a service limit. - type: int - max_retries: - description: - - The maximum number of times to retry this job if it fails. - type: int - name: - description: - - The name you assign to this job definition. It must be unique in your account. - required: true - type: str - role: - description: - - The name or ARN of the IAM role associated with this job. - - Required when I(state=present). - type: str - state: - description: - - Create or delete the AWS Glue job. - required: true - choices: [ 'present', 'absent' ] - type: str - timeout: - description: - - The job timeout in minutes. - type: int -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an AWS Glue job -- aws_glue_job: - command_script_location: s3bucket/script.py - name: my-glue-job - role: my-iam-role - state: present - -# Delete an AWS Glue job -- aws_glue_job: - name: my-glue-job - state: absent - -''' - -RETURN = ''' -allocated_capacity: - description: The number of AWS Glue data processing units (DPUs) allocated to runs of this job. From 2 to - 100 DPUs can be allocated; the default is 10. A DPU is a relative measure of processing power - that consists of 4 vCPUs of compute capacity and 16 GB of memory. - returned: when state is present - type: int - sample: 10 -command: - description: The JobCommand that executes this job. - returned: when state is present - type: complex - contains: - name: - description: The name of the job command. - returned: when state is present - type: str - sample: glueetl - script_location: - description: Specifies the S3 path to a script that executes a job. - returned: when state is present - type: str - sample: mybucket/myscript.py -connections: - description: The connections used for this job. - returned: when state is present - type: dict - sample: "{ Connections: [ 'list', 'of', 'connections' ] }" -created_on: - description: The time and date that this job definition was created. - returned: when state is present - type: str - sample: "2018-04-21T05:19:58.326000+00:00" -default_arguments: - description: The default arguments for this job, specified as name-value pairs. - returned: when state is present - type: dict - sample: "{ 'mykey1': 'myvalue1' }" -description: - description: Description of the job being defined. - returned: when state is present - type: str - sample: My first Glue job -job_name: - description: The name of the AWS Glue job. - returned: always - type: str - sample: my-glue-job -execution_property: - description: An ExecutionProperty specifying the maximum number of concurrent runs allowed for this job. - returned: always - type: complex - contains: - max_concurrent_runs: - description: The maximum number of concurrent runs allowed for the job. The default is 1. An error is - returned when this threshold is reached. The maximum value you can specify is controlled by - a service limit. - returned: when state is present - type: int - sample: 1 -last_modified_on: - description: The last point in time when this job definition was modified. - returned: when state is present - type: str - sample: "2018-04-21T05:19:58.326000+00:00" -max_retries: - description: The maximum number of times to retry this job after a JobRun fails. - returned: when state is present - type: int - sample: 5 -name: - description: The name assigned to this job definition. - returned: when state is present - type: str - sample: my-glue-job -role: - description: The name or ARN of the IAM role associated with this job. - returned: when state is present - type: str - sample: my-iam-role -timeout: - description: The job timeout in minutes. - returned: when state is present - type: int - sample: 300 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -# Non-ansible imports -import copy -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass - - -def _get_glue_job(connection, module, glue_job_name): - """ - Get an AWS Glue job based on name. If not found, return None. - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :param glue_job_name: Name of Glue job to get - :return: boto3 Glue job dict or None if not found - """ - - try: - return connection.get_job(JobName=glue_job_name)['Job'] - except (BotoCoreError, ClientError) as e: - if e.response['Error']['Code'] == 'EntityNotFoundException': - return None - else: - module.fail_json_aws(e) - - -def _compare_glue_job_params(user_params, current_params): - """ - Compare Glue job params. If there is a difference, return True immediately else return False - - :param user_params: the Glue job parameters passed by the user - :param current_params: the Glue job parameters currently configured - :return: True if any parameter is mismatched else False - """ - - # Weirdly, boto3 doesn't return some keys if the value is empty e.g. Description - # To counter this, add the key if it's missing with a blank value - - if 'Description' not in current_params: - current_params['Description'] = "" - if 'DefaultArguments' not in current_params: - current_params['DefaultArguments'] = dict() - - if 'AllocatedCapacity' in user_params and user_params['AllocatedCapacity'] != current_params['AllocatedCapacity']: - return True - if 'Command' in user_params and user_params['Command']['ScriptLocation'] != current_params['Command']['ScriptLocation']: - return True - if 'Connections' in user_params and set(user_params['Connections']) != set(current_params['Connections']): - return True - if 'DefaultArguments' in user_params and set(user_params['DefaultArguments']) != set(current_params['DefaultArguments']): - return True - if 'Description' in user_params and user_params['Description'] != current_params['Description']: - return True - if 'ExecutionProperty' in user_params and user_params['ExecutionProperty']['MaxConcurrentRuns'] != current_params['ExecutionProperty']['MaxConcurrentRuns']: - return True - if 'MaxRetries' in user_params and user_params['MaxRetries'] != current_params['MaxRetries']: - return True - if 'Timeout' in user_params and user_params['Timeout'] != current_params['Timeout']: - return True - - return False - - -def create_or_update_glue_job(connection, module, glue_job): - """ - Create or update an AWS Glue job - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :param glue_job: a dict of AWS Glue job parameters or None - :return: - """ - - changed = False - params = dict() - params['Name'] = module.params.get("name") - params['Role'] = module.params.get("role") - if module.params.get("allocated_capacity") is not None: - params['AllocatedCapacity'] = module.params.get("allocated_capacity") - if module.params.get("command_script_location") is not None: - params['Command'] = {'Name': module.params.get("command_name"), 'ScriptLocation': module.params.get("command_script_location")} - if module.params.get("connections") is not None: - params['Connections'] = {'Connections': module.params.get("connections")} - if module.params.get("default_arguments") is not None: - params['DefaultArguments'] = module.params.get("default_arguments") - if module.params.get("description") is not None: - params['Description'] = module.params.get("description") - if module.params.get("max_concurrent_runs") is not None: - params['ExecutionProperty'] = {'MaxConcurrentRuns': module.params.get("max_concurrent_runs")} - if module.params.get("max_retries") is not None: - params['MaxRetries'] = module.params.get("max_retries") - if module.params.get("timeout") is not None: - params['Timeout'] = module.params.get("timeout") - - # If glue_job is not None then check if it needs to be modified, else create it - if glue_job: - if _compare_glue_job_params(params, glue_job): - try: - # Update job needs slightly modified params - update_params = {'JobName': params['Name'], 'JobUpdate': copy.deepcopy(params)} - del update_params['JobUpdate']['Name'] - connection.update_job(**update_params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - else: - try: - connection.create_job(**params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - # If changed, get the Glue job again - if changed: - glue_job = _get_glue_job(connection, module, params['Name']) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(glue_job)) - - -def delete_glue_job(connection, module, glue_job): - """ - Delete an AWS Glue job - - :param connection: AWS boto3 glue connection - :param module: Ansible module - :param glue_job: a dict of AWS Glue job parameters or None - :return: - """ - - changed = False - - if glue_job: - try: - connection.delete_job(JobName=glue_job['Name']) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - module.exit_json(changed=changed) - - -def main(): - - argument_spec = ( - dict( - allocated_capacity=dict(type='int'), - command_name=dict(type='str', default='glueetl'), - command_script_location=dict(type='str'), - connections=dict(type='list'), - default_arguments=dict(type='dict'), - description=dict(type='str'), - max_concurrent_runs=dict(type='int'), - max_retries=dict(type='int'), - name=dict(required=True, type='str'), - role=dict(type='str'), - state=dict(required=True, choices=['present', 'absent'], type='str'), - timeout=dict(type='int') - ) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[ - ('state', 'present', ['role', 'command_script_location']) - ] - ) - - connection = module.client('glue') - - state = module.params.get("state") - - glue_job = _get_glue_job(connection, module, module.params.get("name")) - - if state == 'present': - create_or_update_glue_job(connection, module, glue_job) - else: - delete_glue_job(connection, module, glue_job) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_inspector_target.py b/lib/ansible/modules/cloud/amazon/aws_inspector_target.py deleted file mode 100644 index a5607b765b..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_inspector_target.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2018 Dennis Conrad for Sainsbury's -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_inspector_target -short_description: Create, Update and Delete Amazon Inspector Assessment - Targets -description: Creates, updates, or deletes Amazon Inspector Assessment Targets - and manages the required Resource Groups. -version_added: "2.6" -author: "Dennis Conrad (@dennisconrad)" -options: - name: - description: - - The user-defined name that identifies the assessment target. The name - must be unique within the AWS account. - required: true - type: str - state: - description: - - The state of the assessment target. - choices: - - absent - - present - default: present - type: str - tags: - description: - - Tags of the EC2 instances to be added to the assessment target. - - Required if C(state=present). - type: dict -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore -''' - -EXAMPLES = ''' -- name: Create my_target Assessment Target - aws_inspector_target: - name: my_target - tags: - role: scan_target - -- name: Update Existing my_target Assessment Target with Additional Tags - aws_inspector_target: - name: my_target - tags: - env: dev - role: scan_target - -- name: Delete my_target Assessment Target - aws_inspector_target: - name: my_target - state: absent -''' - -RETURN = ''' -arn: - description: The ARN that specifies the Amazon Inspector assessment target. - returned: success - type: str - sample: "arn:aws:inspector:eu-west-1:123456789012:target/0-O4LnL7n1" -created_at: - description: The time at which the assessment target was created. - returned: success - type: str - sample: "2018-01-29T13:48:51.958000+00:00" -name: - description: The name of the Amazon Inspector assessment target. - returned: success - type: str - sample: "my_target" -resource_group_arn: - description: The ARN that specifies the resource group that is associated - with the assessment target. - returned: success - type: str - sample: "arn:aws:inspector:eu-west-1:123456789012:resourcegroup/0-qY4gDel8" -tags: - description: The tags of the resource group that is associated with the - assessment target. - returned: success - type: list - sample: {"role": "scan_target", "env": "dev"} -updated_at: - description: The time at which the assessment target was last updated. - returned: success - type: str - sample: "2018-01-29T13:48:51.958000+00:00" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry -from ansible.module_utils.ec2 import ( - ansible_dict_to_boto3_tag_list, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - compare_aws_tags, -) - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -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, - ) - - 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_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_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_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_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_aws(e, msg="trying to delete target") - - elif state == 'absent' and not target_exists: - module.exit_json(changed=False) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_kms.py b/lib/ansible/modules/cloud/amazon/aws_kms.py deleted file mode 100644 index 8a906a9f3d..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_kms.py +++ /dev/null @@ -1,1072 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -* -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_kms -short_description: Perform various KMS management tasks. -description: - - Manage role/user access to a KMS key. Not designed for encrypting/decrypting. -version_added: "2.3" -options: - alias: - description: An alias for a key. For safety, even though KMS does not require keys - to have an alias, this module expects all new keys to be given an alias - to make them easier to manage. Existing keys without an alias may be - referred to by I(key_id). Use M(aws_kms_info) to find key ids. Required - if I(key_id) is not given. Note that passing a I(key_id) and I(alias) - will only cause a new alias to be added, an alias will never be renamed. - The 'alias/' prefix is optional. - required: false - aliases: - - key_alias - type: str - key_id: - description: - - Key ID or ARN of the key. - - One of I(alias) or I(key_id) are required. - required: false - aliases: - - key_arn - type: str - enable_key_rotation: - description: - - Whether the key should be automatically rotated every year. - required: false - type: bool - version_added: '2.10' - policy_mode: - description: - - (deprecated) Grant or deny access. - - Used for modifying the Key Policy rather than modifying a grant and only - works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. - default: grant - choices: [ grant, deny ] - aliases: - - mode - type: str - policy_role_name: - description: - - (deprecated) Role to allow/deny access. - - One of I(policy_role_name) or I(policy_role_arn) are required. - - Used for modifying the Key Policy rather than modifying a grant and only - works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. - required: false - aliases: - - role_name - type: str - policy_role_arn: - description: - - (deprecated) ARN of role to allow/deny access. - - One of I(policy_role_name) or I(policy_role_arn) are required. - - Used for modifying the Key Policy rather than modifying a grant and only - works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. - type: str - required: false - aliases: - - role_arn - policy_grant_types: - description: - - (deprecated) List of grants to give to user/role. Likely "role,role grant" or "role,role grant,admin". - - Required when I(policy_mode=grant). - - Used for modifying the Key Policy rather than modifying a grant and only - works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. - required: false - aliases: - - grant_types - type: list - elements: str - policy_clean_invalid_entries: - description: - - (deprecated) If adding/removing a role and invalid grantees are found, remove them. These entries will cause an update to fail in all known cases. - - Only cleans if changes are being made. - - Used for modifying the Key Policy rather than modifying a grant and only - works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. - type: bool - default: true - aliases: - - clean_invalid_entries - state: - description: Whether a key should be present or absent. Note that making an - existing key absent only schedules a key for deletion. Passing a key that - is scheduled for deletion with state present will cancel key deletion. - required: False - choices: - - present - - absent - default: present - version_added: 2.8 - type: str - enabled: - description: Whether or not a key is enabled - default: True - version_added: 2.8 - type: bool - description: - description: - A description of the CMK. Use a description that helps you decide - whether the CMK is appropriate for a task. - version_added: 2.8 - type: str - tags: - description: A dictionary of tags to apply to a key. - version_added: 2.8 - type: dict - purge_tags: - description: Whether the I(tags) argument should cause tags not in the list to - be removed - version_added: 2.8 - default: False - type: bool - purge_grants: - description: Whether the I(grants) argument should cause grants not in the list to - be removed - default: False - version_added: 2.8 - type: bool - grants: - description: - - A list of grants to apply to the key. Each item must contain I(grantee_principal). - Each item can optionally contain I(retiring_principal), I(operations), I(constraints), - I(name). - - I(grantee_principal) and I(retiring_principal) must be ARNs - - 'For full documentation of suboptions see the boto3 documentation:' - - 'U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.create_grant)' - version_added: 2.8 - type: list - elements: dict - suboptions: - grantee_principal: - description: The full ARN of the principal being granted permissions. - required: true - type: str - retiring_principal: - description: The full ARN of the principal permitted to revoke/retire the grant. - type: str - operations: - type: list - elements: str - description: - - A list of operations that the grantee may perform using the CMK. - choices: ['Decrypt', 'Encrypt', 'GenerateDataKey', 'GenerateDataKeyWithoutPlaintext', 'ReEncryptFrom', 'ReEncryptTo', - 'CreateGrant', 'RetireGrant', 'DescribeKey', 'Verify', 'Sign'] - constraints: - description: - - Constraints is a dict containing C(encryption_context_subset) or C(encryption_context_equals), - either or both being a dict specifying an encryption context match. - See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) or - U(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.create_grant) - type: dict - policy: - description: - - policy to apply to the KMS key - - See U(https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html) - type: str - version_added: 2.8 -author: - - Ted Timmons (@tedder) - - Will Thames (@willthames) - - Mark Chappell (@tremble) -extends_documentation_fragment: -- aws -- ec2 -''' - -EXAMPLES = ''' -# Managing the KMS IAM Policy via policy_mode and policy_grant_types is fragile -# and has been deprecated in favour of the policy option. -- name: grant user-style access to production secrets - aws_kms: - args: - alias: "alias/my_production_secrets" - policy_mode: grant - policy_role_name: "prod-appServerRole-1R5AQG2BSEL6L" - policy_grant_types: "role,role grant" -- name: remove access to production secrets from role - aws_kms: - args: - alias: "alias/my_production_secrets" - policy_mode: deny - policy_role_name: "prod-appServerRole-1R5AQG2BSEL6L" - -# Create a new KMS key -- aws_kms: - alias: mykey - tags: - Name: myKey - Purpose: protect_stuff - -# Update previous key with more tags -- aws_kms: - alias: mykey - tags: - Name: myKey - Purpose: protect_stuff - Owner: security_team - -# Update a known key with grants allowing an instance with the billing-prod IAM profile -# to decrypt data encrypted with the environment: production, application: billing -# encryption context -- aws_kms: - key_id: abcd1234-abcd-1234-5678-ef1234567890 - grants: - - name: billing_prod - grantee_principal: arn:aws:iam::1234567890123:role/billing_prod - constraints: - encryption_context_equals: - environment: production - application: billing - operations: - - Decrypt - - RetireGrant -''' - -RETURN = ''' -key_id: - description: ID of key - type: str - returned: always - sample: abcd1234-abcd-1234-5678-ef1234567890 -key_arn: - description: ARN of key - type: str - returned: always - sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 -key_state: - description: The state of the key - type: str - returned: always - sample: PendingDeletion -key_usage: - description: The cryptographic operations for which you can use the key. - type: str - returned: always - sample: ENCRYPT_DECRYPT -origin: - description: The source of the key's key material. When this value is C(AWS_KMS), - AWS KMS created the key material. When this value is C(EXTERNAL), the - key material was imported or the CMK lacks key material. - type: str - returned: always - sample: AWS_KMS -aws_account_id: - description: The AWS Account ID that the key belongs to - type: str - returned: always - sample: 1234567890123 -creation_date: - description: Date of creation of the key - type: str - returned: always - sample: "2017-04-18T15:12:08.551000+10:00" -description: - description: Description of the key - type: str - returned: always - sample: "My Key for Protecting important stuff" -enabled: - description: Whether the key is enabled. True if C(KeyState) is true. - type: str - returned: always - sample: false -aliases: - description: list of aliases associated with the key - type: list - returned: always - sample: - - aws/acm - - aws/ebs -policies: - description: list of policy documents for the keys. Empty when access is denied even if there are policies. - type: list - returned: always - sample: - Version: "2012-10-17" - Id: "auto-ebs-2" - Statement: - - Sid: "Allow access through EBS for all principals in the account that are authorized to use EBS" - Effect: "Allow" - Principal: - AWS: "*" - Action: - - "kms:Encrypt" - - "kms:Decrypt" - - "kms:ReEncrypt*" - - "kms:GenerateDataKey*" - - "kms:CreateGrant" - - "kms:DescribeKey" - Resource: "*" - Condition: - StringEquals: - kms:CallerAccount: "111111111111" - kms:ViaService: "ec2.ap-southeast-2.amazonaws.com" - - Sid: "Allow direct access to key metadata to the account" - Effect: "Allow" - Principal: - AWS: "arn:aws:iam::111111111111:root" - Action: - - "kms:Describe*" - - "kms:Get*" - - "kms:List*" - - "kms:RevokeGrant" - Resource: "*" -tags: - description: dictionary of tags applied to the key - type: dict - returned: always - sample: - Name: myKey - Purpose: protecting_stuff -grants: - description: list of grants associated with a key - type: complex - returned: always - contains: - constraints: - description: Constraints on the encryption context that the grant allows. - See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) for further details - type: dict - returned: always - sample: - encryption_context_equals: - "aws:lambda:_function_arn": "arn:aws:lambda:ap-southeast-2:012345678912:function:xyz" - creation_date: - description: Date of creation of the grant - type: str - returned: always - sample: "2017-04-18T15:12:08+10:00" - grant_id: - description: The unique ID for the grant - type: str - returned: always - sample: abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234 - grantee_principal: - description: The principal that receives the grant's permissions - type: str - returned: always - sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz - issuing_account: - description: The AWS account under which the grant was issued - type: str - returned: always - sample: arn:aws:iam::01234567890:root - key_id: - description: The key ARN to which the grant applies. - type: str - returned: always - sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 - name: - description: The friendly name that identifies the grant - type: str - returned: always - sample: xyz - operations: - description: The list of operations permitted by the grant - type: list - returned: always - sample: - - Decrypt - - RetireGrant - retiring_principal: - description: The principal that can retire the grant - type: str - returned: always - sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz -changes_needed: - description: grant types that would be changed/were changed. - type: dict - returned: always - sample: { "role": "add", "role grant": "add" } -had_invalid_entries: - description: there are invalid (non-ARN) entries in the KMS entry. These don't count as a change, but will be removed if any changes are being made. - type: bool - returned: always -''' - -# these mappings are used to go from simple labels to the actual 'Sid' values returned -# by get_policy. They seem to be magic values. -statement_label = { - 'role': 'Allow use of the key', - 'role grant': 'Allow attachment of persistent resources', - 'admin': 'Allow access for Key Administrators' -} - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list -from ansible.module_utils.ec2 import compare_aws_tags, compare_policies -from ansible.module_utils.six import string_types - -import json - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_iam_roles_with_backoff(connection): - paginator = connection.get_paginator('list_roles') - return paginator.paginate().build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_keys_with_backoff(connection): - paginator = connection.get_paginator('list_keys') - return paginator.paginate().build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_aliases_with_backoff(connection): - paginator = connection.get_paginator('list_aliases') - return paginator.paginate().build_full_result() - - -def get_kms_aliases_lookup(connection): - _aliases = dict() - for alias in get_kms_aliases_with_backoff(connection)['Aliases']: - # Not all aliases are actually associated with a key - if 'TargetKeyId' in alias: - # strip off leading 'alias/' and add it to key's aliases - if alias['TargetKeyId'] in _aliases: - _aliases[alias['TargetKeyId']].append(alias['AliasName'][6:]) - else: - _aliases[alias['TargetKeyId']] = [alias['AliasName'][6:]] - return _aliases - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_tags_with_backoff(connection, key_id, **kwargs): - return connection.list_resource_tags(KeyId=key_id, **kwargs) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_grants_with_backoff(connection, key_id): - params = dict(KeyId=key_id) - paginator = connection.get_paginator('list_grants') - return paginator.paginate(**params).build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_metadata_with_backoff(connection, key_id): - return connection.describe_key(KeyId=key_id) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_key_policies_with_backoff(connection, key_id): - paginator = connection.get_paginator('list_key_policies') - return paginator.paginate(KeyId=key_id).build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_key_policy_with_backoff(connection, key_id, policy_name): - return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name) - - -def get_kms_tags(connection, module, key_id): - # Handle pagination here as list_resource_tags does not have - # a paginator - kwargs = {} - tags = [] - more = True - while more: - try: - tag_response = get_kms_tags_with_backoff(connection, key_id, **kwargs) - tags.extend(tag_response['Tags']) - except is_boto3_error_code('AccessDeniedException'): - tag_response = {} - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Failed to obtain key tags") - if tag_response.get('NextMarker'): - kwargs['Marker'] = tag_response['NextMarker'] - else: - more = False - return tags - - -def get_kms_policies(connection, module, key_id): - try: - policies = list_key_policies_with_backoff(connection, key_id)['PolicyNames'] - return [get_key_policy_with_backoff(connection, key_id, policy)['Policy'] for - policy in policies] - except is_boto3_error_code('AccessDeniedException'): - return [] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Failed to obtain key policies") - - -def camel_to_snake_grant(grant): - ''' camel_to_snake_grant snakifies everything except the encryption context ''' - constraints = grant.get('Constraints', {}) - result = camel_dict_to_snake_dict(grant) - if 'EncryptionContextEquals' in constraints: - result['constraints']['encryption_context_equals'] = constraints['EncryptionContextEquals'] - if 'EncryptionContextSubset' in constraints: - result['constraints']['encryption_context_subset'] = constraints['EncryptionContextSubset'] - return result - - -def get_key_details(connection, module, key_id): - try: - result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to obtain key metadata") - result['KeyArn'] = result.pop('Arn') - - try: - aliases = get_kms_aliases_lookup(connection) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to obtain aliases") - - current_rotation_status = connection.get_key_rotation_status(KeyId=key_id) - result['enable_key_rotation'] = current_rotation_status.get('KeyRotationEnabled') - result['aliases'] = aliases.get(result['KeyId'], []) - - result = camel_dict_to_snake_dict(result) - - # grants and tags get snakified differently - try: - result['grants'] = [camel_to_snake_grant(grant) for grant in - get_kms_grants_with_backoff(connection, key_id)['Grants']] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to obtain key grants") - tags = get_kms_tags(connection, module, key_id) - result['tags'] = boto3_tag_list_to_ansible_dict(tags, 'TagKey', 'TagValue') - result['policies'] = get_kms_policies(connection, module, key_id) - return result - - -def get_kms_facts(connection, module): - try: - keys = get_kms_keys_with_backoff(connection)['Keys'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to obtain keys") - - return [get_key_details(connection, module, key['KeyId']) for key in keys] - - -def convert_grant_params(grant, key): - grant_params = dict(KeyId=key['key_arn'], - GranteePrincipal=grant['grantee_principal']) - if grant.get('operations'): - grant_params['Operations'] = grant['operations'] - if grant.get('retiring_principal'): - grant_params['RetiringPrincipal'] = grant['retiring_principal'] - if grant.get('name'): - grant_params['Name'] = grant['name'] - if grant.get('constraints'): - grant_params['Constraints'] = dict() - if grant['constraints'].get('encryption_context_subset'): - grant_params['Constraints']['EncryptionContextSubset'] = grant['constraints']['encryption_context_subset'] - if grant['constraints'].get('encryption_context_equals'): - grant_params['Constraints']['EncryptionContextEquals'] = grant['constraints']['encryption_context_equals'] - return grant_params - - -def different_grant(existing_grant, desired_grant): - if existing_grant.get('grantee_principal') != desired_grant.get('grantee_principal'): - return True - if existing_grant.get('retiring_principal') != desired_grant.get('retiring_principal'): - return True - if set(existing_grant.get('operations', [])) != set(desired_grant.get('operations')): - return True - if existing_grant.get('constraints') != desired_grant.get('constraints'): - return True - return False - - -def compare_grants(existing_grants, desired_grants, purge_grants=False): - existing_dict = dict((eg['name'], eg) for eg in existing_grants) - desired_dict = dict((dg['name'], dg) for dg in desired_grants) - to_add_keys = set(desired_dict.keys()) - set(existing_dict.keys()) - if purge_grants: - to_remove_keys = set(existing_dict.keys()) - set(desired_dict.keys()) - else: - to_remove_keys = set() - to_change_candidates = set(existing_dict.keys()) & set(desired_dict.keys()) - for candidate in to_change_candidates: - if different_grant(existing_dict[candidate], desired_dict[candidate]): - to_add_keys.add(candidate) - to_remove_keys.add(candidate) - - to_add = [] - to_remove = [] - for key in to_add_keys: - grant = desired_dict[key] - to_add.append(grant) - for key in to_remove_keys: - grant = existing_dict[key] - to_remove.append(grant) - return to_add, to_remove - - -def start_key_deletion(connection, module, key_metadata): - if key_metadata['KeyState'] == 'PendingDeletion': - return False - - if module.check_mode: - return True - - try: - connection.schedule_key_deletion(KeyId=key_metadata['Arn']) - return True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to schedule key for deletion") - - -def cancel_key_deletion(connection, module, key): - key_id = key['key_arn'] - if key['key_state'] != 'PendingDeletion': - return False - - if module.check_mode: - return True - - try: - connection.cancel_key_deletion(KeyId=key_id) - # key is disabled after deletion cancellation - # set this so that ensure_enabled_disabled works correctly - key['key_state'] = 'Disabled' - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to cancel key deletion") - - return True - - -def ensure_enabled_disabled(connection, module, key, enabled): - desired_state = 'Enabled' - if not enabled: - desired_state = 'Disabled' - - if key['key_state'] == desired_state: - return False - - key_id = key['key_arn'] - if not module.check_mode: - if enabled: - try: - connection.enable_key(KeyId=key_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to enable key") - else: - try: - connection.disable_key(KeyId=key_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to disable key") - - return True - - -def update_alias(connection, module, key, alias): - alias = canonicalize_alias_name(alias) - - if alias is None: - return False - - key_id = key['key_arn'] - aliases = get_kms_aliases_with_backoff(connection)['Aliases'] - # We will only add new aliases, not rename existing ones - if alias in [_alias['AliasName'] for _alias in aliases]: - return False - - if not module.check_mode: - try: - connection.create_alias(TargetKeyId=key_id, AliasName=alias) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed create key alias") - - return True - - -def update_description(connection, module, key, description): - if description is None: - return False - if key['description'] == description: - return False - - key_id = key['key_arn'] - if not module.check_mode: - try: - connection.update_key_description(KeyId=key_id, Description=description) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to update key description") - - return True - - -def update_tags(connection, module, key, desired_tags, purge_tags): - # purge_tags needs to be explicitly set, so an empty tags list means remove - # all tags - - to_add, to_remove = compare_aws_tags(key['tags'], desired_tags, purge_tags) - if not (bool(to_add) or bool(to_remove)): - return False - - key_id = key['key_arn'] - if not module.check_mode: - if to_remove: - try: - connection.untag_resource(KeyId=key_id, TagKeys=to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to remove tag") - if to_add: - try: - tags = ansible_dict_to_boto3_tag_list(module.params['tags'], tag_name_key_name='TagKey', tag_value_key_name='TagValue') - connection.tag_resource(KeyId=key_id, Tags=tags) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to add tag to key") - - return True - - -def update_policy(connection, module, key, policy): - if policy is None: - return False - try: - new_policy = json.loads(policy) - except ValueError as e: - module.fail_json_aws(e, msg="Unable to parse new policy as JSON") - - key_id = key['key_arn'] - try: - keyret = connection.get_key_policy(KeyId=key_id, PolicyName='default') - original_policy = json.loads(keyret['Policy']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError): - # If we can't fetch the current policy assume we're making a change - # Could occur if we have PutKeyPolicy without GetKeyPolicy - original_policy = {} - - if not compare_policies(original_policy, new_policy): - return False - - if not module.check_mode: - try: - connection.put_key_policy(KeyId=key_id, PolicyName='default', Policy=policy) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to update key policy") - - return True - - -def update_key_rotation(connection, module, key, enable_key_rotation): - if enable_key_rotation is None: - return False - key_id = key['key_arn'] - current_rotation_status = connection.get_key_rotation_status(KeyId=key_id) - if current_rotation_status.get('KeyRotationEnabled') == enable_key_rotation: - return False - - if enable_key_rotation: - connection.enable_key_rotation(KeyId=key_id) - else: - connection.disable_key_rotation(KeyId=key_id) - return True - - -def update_grants(connection, module, key, desired_grants, purge_grants): - existing_grants = key['grants'] - - to_add, to_remove = compare_grants(existing_grants, desired_grants, purge_grants) - if not (bool(to_add) or bool(to_remove)): - return False - - key_id = key['key_arn'] - if not module.check_mode: - for grant in to_remove: - try: - connection.retire_grant(KeyId=key_id, GrantId=grant['grant_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to retire grant") - for grant in to_add: - 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_aws(e, msg="Unable to create grant") - - return True - - -def update_key(connection, module, key): - changed = False - - changed |= cancel_key_deletion(connection, module, key) - changed |= ensure_enabled_disabled(connection, module, key, module.params['enabled']) - changed |= update_alias(connection, module, key, module.params['alias']) - changed |= update_description(connection, module, key, module.params['description']) - changed |= update_tags(connection, module, key, module.params['tags'], module.params.get('purge_tags')) - changed |= update_policy(connection, module, key, module.params.get('policy')) - changed |= update_grants(connection, module, key, module.params.get('grants'), module.params.get('purge_grants')) - changed |= update_key_rotation(connection, module, key, module.params.get('enable_key_rotation')) - - # make results consistent with kms_facts before returning - result = get_key_details(connection, module, key['key_arn']) - result['changed'] = changed - return result - - -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_aws(e, msg="Failed to create initial key") - key = get_key_details(connection, module, result['KeyId']) - - update_alias(connection, module, key, module.params['alias']) - update_key_rotation(connection, module, key, module.params.get('enable_key_rotation')) - - ensure_enabled_disabled(connection, module, key, module.params.get('enabled')) - update_grants(connection, module, key, module.params.get('grants'), False) - - # make results consistent with kms_facts - result = get_key_details(connection, module, key['key_id']) - result['changed'] = True - return result - - -def delete_key(connection, module, key_metadata): - changed = False - - changed |= start_key_deletion(connection, module, key_metadata) - - result = get_key_details(connection, module, key_metadata['Arn']) - result['changed'] = changed - return result - - -def get_arn_from_role_name(iam, rolename): - ret = iam.get_role(RoleName=rolename) - if ret.get('Role') and ret['Role'].get('Arn'): - return ret['Role']['Arn'] - raise Exception('could not find arn for name {0}.'.format(rolename)) - - -def _clean_statement_principals(statement, clean_invalid_entries): - - # create Principal and 'AWS' so we can safely use them later. - if not isinstance(statement.get('Principal'), dict): - statement['Principal'] = dict() - - # If we have a single AWS Principal, ensure we still have a list (to manipulate) - if 'AWS' in statement['Principal'] and isinstance(statement['Principal']['AWS'], string_types): - statement['Principal']['AWS'] = [statement['Principal']['AWS']] - if not isinstance(statement['Principal'].get('AWS'), list): - statement['Principal']['AWS'] = list() - - invalid_entries = [item for item in statement['Principal']['AWS'] if not item.startswith('arn:aws:iam::')] - valid_entries = [item for item in statement['Principal']['AWS'] if item.startswith('arn:aws:iam::')] - - if bool(invalid_entries) and clean_invalid_entries: - statement['Principal']['AWS'] = valid_entries - return True - - return False - - -def _do_statement_grant(statement, role_arn, grant_types, mode, grant_type): - - if mode == 'grant': - if grant_type in grant_types: - if role_arn not in statement['Principal']['AWS']: # needs to be added. - statement['Principal']['AWS'].append(role_arn) - return 'add' - elif role_arn in statement['Principal']['AWS']: # not one the places the role should be - statement['Principal']['AWS'].remove(role_arn) - return 'remove' - return None - - if mode == 'deny' and role_arn in statement['Principal']['AWS']: - # we don't selectively deny. that's a grant with a - # smaller list. so deny=remove all of this arn. - statement['Principal']['AWS'].remove(role_arn) - return 'remove' - return None - - -def do_policy_grant(module, kms, keyarn, role_arn, grant_types, mode='grant', dry_run=True, clean_invalid_entries=True): - ret = {} - policy = json.loads(get_key_policy_with_backoff(kms, keyarn, 'default')['Policy']) - - changes_needed = {} - assert_policy_shape(module, policy) - had_invalid_entries = False - for statement in policy['Statement']: - # We already tested that these are the only types in the statements - for grant_type in statement_label: - # Are we on this grant type's statement? - if statement['Sid'] != statement_label[grant_type]: - continue - - had_invalid_entries |= _clean_statement_principals(statement, clean_invalid_entries) - change = _do_statement_grant(statement, role_arn, grant_types, mode, grant_type) - if change: - changes_needed[grant_type] = change - - ret['changes_needed'] = changes_needed - ret['had_invalid_entries'] = had_invalid_entries - ret['new_policy'] = policy - ret['changed'] = bool(changes_needed) - - if dry_run or not ret['changed']: - return ret - - try: - policy_json_string = json.dumps(policy) - kms.put_key_policy(KeyId=keyarn, PolicyName='default', Policy=policy_json_string) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not update key_policy', new_policy=policy_json_string) - - return ret - - -def assert_policy_shape(module, policy): - '''Since the policy seems a little, uh, fragile, make sure we know approximately what we're looking at.''' - errors = [] - if policy['Version'] != "2012-10-17": - errors.append('Unknown version/date ({0}) of policy. Things are probably different than we assumed they were.'.format(policy['Version'])) - - found_statement_type = {} - for statement in policy['Statement']: - for label, sidlabel in statement_label.items(): - if statement['Sid'] == sidlabel: - found_statement_type[label] = True - - for statementtype in statement_label: - if not found_statement_type.get(statementtype): - errors.append('Policy is missing {0}.'.format(statementtype)) - - if errors: - module.fail_json(msg='Problems asserting policy shape. Cowardly refusing to modify it', errors=errors, policy=policy) - - -def canonicalize_alias_name(alias): - if alias is None: - return None - if alias.startswith('alias/'): - return alias - return 'alias/' + alias - - -def fetch_key_metadata(connection, module, key_id, alias): - - alias = canonicalize_alias_name(module.params.get('alias')) - - try: - # Fetch by key_id where possible - if key_id: - return get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata'] - # Or try alias as a backup - return get_kms_metadata_with_backoff(connection, alias)['KeyMetadata'] - - except connection.exceptions.NotFoundException: - return None - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, 'Failed to fetch key metadata.') - - -def update_policy_grants(connection, module, key_metadata, mode): - iam = module.client('iam') - key_id = key_metadata['Arn'] - - if module.params.get('policy_role_name') and not module.params.get('policy_role_arn'): - module.params['policy_role_arn'] = get_arn_from_role_name(iam, module.params['policy_role_name']) - if not module.params.get('policy_role_arn'): - module.fail_json(msg='policy_role_arn or policy_role_name is required to {0}'.format(module.params['policy_mode'])) - - # check the grant types for 'grant' only. - if mode == 'grant': - for grant_type in module.params['policy_grant_types']: - if grant_type not in statement_label: - module.fail_json(msg='{0} is an unknown grant type.'.format(grant_type)) - - return do_policy_grant(module, connection, - key_id, - module.params['policy_role_arn'], - module.params['policy_grant_types'], - mode=mode, - dry_run=module.check_mode, - clean_invalid_entries=module.params['policy_clean_invalid_entries']) - - -def main(): - argument_spec = dict( - alias=dict(aliases=['key_alias']), - policy_mode=dict(aliases=['mode'], choices=['grant', 'deny'], default='grant'), - policy_role_name=dict(aliases=['role_name']), - policy_role_arn=dict(aliases=['role_arn']), - policy_grant_types=dict(aliases=['grant_types'], type='list'), - policy_clean_invalid_entries=dict(aliases=['clean_invalid_entries'], type='bool', default=True), - key_id=dict(aliases=['key_arn']), - description=dict(), - enabled=dict(type='bool', default=True), - tags=dict(type='dict', default={}), - purge_tags=dict(type='bool', default=False), - grants=dict(type='list', default=[]), - policy=dict(), - purge_grants=dict(type='bool', default=False), - state=dict(default='present', choices=['present', 'absent']), - enable_key_rotation=(dict(type='bool')) - ) - - module = AnsibleAWSModule( - supports_check_mode=True, - argument_spec=argument_spec, - required_one_of=[['alias', 'key_id']], - ) - - mode = module.params['policy_mode'] - - kms = module.client('kms') - - key_metadata = fetch_key_metadata(kms, module, module.params.get('key_id'), module.params.get('alias')) - # We can't create keys with a specific ID, if we can't access the key we'll have to fail - if module.params.get('state') == 'present' and module.params.get('key_id') and not key_metadata: - module.fail_json(msg="Could not find key with id %s to update") - - if module.params.get('policy_grant_types') or mode == 'deny': - module.deprecate('Managing the KMS IAM Policy via policy_mode and policy_grant_types is fragile' - ' and has been deprecated in favour of the policy option.', version='2.13') - result = update_policy_grants(kms, module, key_metadata, mode) - module.exit_json(**result) - - if module.params.get('state') == 'absent': - if key_metadata is None: - module.exit_json(changed=False) - result = delete_key(kms, module, key_metadata) - module.exit_json(**result) - - if key_metadata: - key_details = get_key_details(kms, module, key_metadata['Arn']) - result = update_key(kms, module, key_details) - module.exit_json(**result) - - result = create_key(kms, module) - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_kms_info.py b/lib/ansible/modules/cloud/amazon/aws_kms_info.py deleted file mode 100644 index e8988b45b1..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_kms_info.py +++ /dev/null @@ -1,433 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: aws_kms_info -short_description: Gather information about AWS KMS keys -description: - - Gather information about AWS KMS keys including tags and grants - - This module was called C(aws_kms_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -author: "Will Thames (@willthames)" -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - The filters aren't natively supported by boto3, but are supported to provide similar - functionality to other modules. Standard tag filters (C(tag-key), C(tag-value) and - C(tag:tagName)) are available, as are C(key-id) and C(alias) - type: dict - pending_deletion: - description: Whether to get full details (tags, grants etc.) of keys pending deletion - default: False - type: bool -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all KMS keys -- aws_kms_info: - -# Gather information about all keys with a Name tag -- aws_kms_info: - filters: - tag-key: Name - -# Gather information about all keys with a specific name -- aws_kms_info: - filters: - "tag:Name": Example -''' - -RETURN = ''' -keys: - description: list of keys - type: complex - returned: always - contains: - key_id: - description: ID of key - type: str - returned: always - sample: abcd1234-abcd-1234-5678-ef1234567890 - key_arn: - description: ARN of key - type: str - returned: always - sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 - key_state: - description: The state of the key - type: str - returned: always - sample: PendingDeletion - key_usage: - description: The cryptographic operations for which you can use the key. - type: str - returned: always - sample: ENCRYPT_DECRYPT - origin: - description: - The source of the key's key material. When this value is C(AWS_KMS), - AWS KMS created the key material. When this value is C(EXTERNAL), the - key material was imported or the CMK lacks key material. - type: str - returned: always - sample: AWS_KMS - aws_account_id: - description: The AWS Account ID that the key belongs to - type: str - returned: always - sample: 1234567890123 - creation_date: - description: Date of creation of the key - type: str - returned: always - sample: "2017-04-18T15:12:08.551000+10:00" - description: - description: Description of the key - type: str - returned: always - sample: "My Key for Protecting important stuff" - enabled: - description: Whether the key is enabled. True if C(KeyState) is true. - type: str - returned: always - sample: false - enable_key_rotation: - description: Whether the automatically key rotation every year is enabled. - type: bool - returned: always - sample: false - aliases: - description: list of aliases associated with the key - type: list - returned: always - sample: - - aws/acm - - aws/ebs - tags: - description: dictionary of tags applied to the key. Empty when access is denied even if there are tags. - type: dict - returned: always - sample: - Name: myKey - Purpose: protecting_stuff - policies: - description: list of policy documents for the keys. Empty when access is denied even if there are policies. - type: list - returned: always - sample: - Version: "2012-10-17" - Id: "auto-ebs-2" - Statement: - - Sid: "Allow access through EBS for all principals in the account that are authorized to use EBS" - Effect: "Allow" - Principal: - AWS: "*" - Action: - - "kms:Encrypt" - - "kms:Decrypt" - - "kms:ReEncrypt*" - - "kms:GenerateDataKey*" - - "kms:CreateGrant" - - "kms:DescribeKey" - Resource: "*" - Condition: - StringEquals: - kms:CallerAccount: "111111111111" - kms:ViaService: "ec2.ap-southeast-2.amazonaws.com" - - Sid: "Allow direct access to key metadata to the account" - Effect: "Allow" - Principal: - AWS: "arn:aws:iam::111111111111:root" - Action: - - "kms:Describe*" - - "kms:Get*" - - "kms:List*" - - "kms:RevokeGrant" - Resource: "*" - grants: - description: list of grants associated with a key - type: complex - returned: always - contains: - constraints: - description: Constraints on the encryption context that the grant allows. - See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) for further details - type: dict - returned: always - sample: - encryption_context_equals: - "aws:lambda:_function_arn": "arn:aws:lambda:ap-southeast-2:012345678912:function:xyz" - creation_date: - description: Date of creation of the grant - type: str - returned: always - sample: "2017-04-18T15:12:08+10:00" - grant_id: - description: The unique ID for the grant - type: str - returned: always - sample: abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234 - grantee_principal: - description: The principal that receives the grant's permissions - type: str - returned: always - sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz - issuing_account: - description: The AWS account under which the grant was issued - type: str - returned: always - sample: arn:aws:iam::01234567890:root - key_id: - description: The key ARN to which the grant applies. - type: str - returned: always - sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 - name: - description: The friendly name that identifies the grant - type: str - returned: always - sample: xyz - operations: - description: The list of operations permitted by the grant - type: list - returned: always - sample: - - Decrypt - - RetireGrant - retiring_principal: - description: The principal that can retire the grant - type: str - returned: always - sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz -''' - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict, HAS_BOTO3 -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict - -import traceback - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -# Caching lookup for aliases -_aliases = dict() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_keys_with_backoff(connection): - paginator = connection.get_paginator('list_keys') - return paginator.paginate().build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_aliases_with_backoff(connection): - paginator = connection.get_paginator('list_aliases') - return paginator.paginate().build_full_result() - - -def get_kms_aliases_lookup(connection): - if not _aliases: - for alias in get_kms_aliases_with_backoff(connection)['Aliases']: - # Not all aliases are actually associated with a key - if 'TargetKeyId' in alias: - # strip off leading 'alias/' and add it to key's aliases - if alias['TargetKeyId'] in _aliases: - _aliases[alias['TargetKeyId']].append(alias['AliasName'][6:]) - else: - _aliases[alias['TargetKeyId']] = [alias['AliasName'][6:]] - return _aliases - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_tags_with_backoff(connection, key_id, **kwargs): - return connection.list_resource_tags(KeyId=key_id, **kwargs) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_grants_with_backoff(connection, key_id, **kwargs): - params = dict(KeyId=key_id) - if kwargs.get('tokens'): - params['GrantTokens'] = kwargs['tokens'] - paginator = connection.get_paginator('list_grants') - return paginator.paginate(**params).build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_kms_metadata_with_backoff(connection, key_id): - return connection.describe_key(KeyId=key_id) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_key_policies_with_backoff(connection, key_id): - paginator = connection.get_paginator('list_key_policies') - return paginator.paginate(KeyId=key_id).build_full_result() - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_key_policy_with_backoff(connection, key_id, policy_name): - return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_enable_key_rotation_with_backoff(connection, key_id): - current_rotation_status = connection.get_key_rotation_status(KeyId=key_id) - return current_rotation_status.get('KeyRotationEnabled') - - -def get_kms_tags(connection, module, key_id): - # Handle pagination here as list_resource_tags does not have - # a paginator - kwargs = {} - tags = [] - more = True - while more: - try: - tag_response = get_kms_tags_with_backoff(connection, key_id, **kwargs) - tags.extend(tag_response['Tags']) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] != 'AccessDeniedException': - module.fail_json(msg="Failed to obtain key tags", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - else: - tag_response = {} - if tag_response.get('NextMarker'): - kwargs['Marker'] = tag_response['NextMarker'] - else: - more = False - return tags - - -def get_kms_policies(connection, module, key_id): - try: - policies = list_key_policies_with_backoff(connection, key_id)['PolicyNames'] - return [get_key_policy_with_backoff(connection, key_id, policy)['Policy'] for - policy in policies] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] != 'AccessDeniedException': - module.fail_json(msg="Failed to obtain key policies", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - else: - return [] - - -def key_matches_filter(key, filtr): - if filtr[0] == 'key-id': - return filtr[1] == key['key_id'] - if filtr[0] == 'tag-key': - return filtr[1] in key['tags'] - if filtr[0] == 'tag-value': - return filtr[1] in key['tags'].values() - if filtr[0] == 'alias': - return filtr[1] in key['aliases'] - if filtr[0].startswith('tag:'): - return key['tags'][filtr[0][4:]] == filtr[1] - - -def key_matches_filters(key, filters): - if not filters: - return True - else: - return all([key_matches_filter(key, filtr) for filtr in filters.items()]) - - -def get_key_details(connection, module, key_id, tokens=None): - if not tokens: - tokens = [] - try: - result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to obtain key metadata", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - result['KeyArn'] = result.pop('Arn') - - try: - aliases = get_kms_aliases_lookup(connection) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to obtain aliases", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - result['aliases'] = aliases.get(result['KeyId'], []) - result['enable_key_rotation'] = get_enable_key_rotation_with_backoff(connection, key_id) - - if module.params.get('pending_deletion'): - return camel_dict_to_snake_dict(result) - - try: - result['grants'] = get_kms_grants_with_backoff(connection, key_id, tokens=tokens)['Grants'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to obtain key grants", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - tags = get_kms_tags(connection, module, key_id) - - result = camel_dict_to_snake_dict(result) - result['tags'] = boto3_tag_list_to_ansible_dict(tags, 'TagKey', 'TagValue') - result['policies'] = get_kms_policies(connection, module, key_id) - return result - - -def get_kms_info(connection, module): - try: - keys = get_kms_keys_with_backoff(connection)['Keys'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to obtain keys", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - return [get_key_details(connection, module, key['KeyId']) for key in keys] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(type='dict'), - pending_deletion=dict(type='bool', default=False) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'aws_kms_facts': - module.deprecate("The 'aws_kms_facts' module has been renamed to 'aws_kms_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='kms', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - all_keys = get_kms_info(connection, module) - module.exit_json(keys=[key for key in all_keys if key_matches_filters(key, module.params['filters'])]) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_region_info.py b/lib/ansible/modules/cloud/amazon/aws_region_info.py deleted file mode 100644 index 8e1ae21681..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_region_info.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = ''' -module: aws_region_info -short_description: Gather information about AWS regions. -description: - - Gather information about AWS regions. - - This module was called C(aws_region_facts) before Ansible 2.9. The usage did not change. -version_added: '2.5' -author: 'Henrique Rodrigues (@Sodki)' -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRegions.html) for - possible filters. Filter names and values are case sensitive. You can also use underscores - instead of dashes (-) in the filter keys, which will take precedence in case of conflict. - default: {} - type: dict -extends_documentation_fragment: - - aws - - ec2 -requirements: [botocore, boto3] -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all regions -- aws_region_info: - -# Gather information about a single region -- aws_region_info: - filters: - region-name: eu-west-1 -''' - -RETURN = ''' -regions: - returned: on success - description: > - Regions that match the provided filters. Each element consists of a dict with all the information related - to that region. - type: list - sample: "[{ - 'endpoint': 'ec2.us-west-1.amazonaws.com', - 'region_name': 'us-west-1' - }]" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry, ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - - -def main(): - argument_spec = dict( - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - if module._name == 'aws_region_facts': - module.deprecate("The 'aws_region_facts' module has been renamed to 'aws_region_info'", version='2.13') - - connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - - # Replace filter key underscores with dashes, for compatibility - sanitized_filters = dict((k.replace('_', '-'), v) for k, v in module.params.get('filters').items()) - - try: - regions = connection.describe_regions( - Filters=ansible_dict_to_boto3_filter_list(sanitized_filters) - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to describe regions.") - - module.exit_json(regions=[camel_dict_to_snake_dict(r) for r in regions['Regions']]) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_s3_bucket_info.py b/lib/ansible/modules/cloud/amazon/aws_s3_bucket_info.py deleted file mode 100644 index 8b5c63f9f1..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_s3_bucket_info.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_s3_bucket_info -short_description: Lists S3 buckets in AWS -requirements: - - boto3 >= 1.4.4 - - python >= 2.6 -description: - - Lists S3 buckets in AWS - - This module was called C(aws_s3_bucket_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(aws_s3_bucket_info) module no longer returns C(ansible_facts)! -version_added: "2.4" -author: "Gerben Geijteman (@hyperized)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Note: Only AWS S3 is currently supported - -# Lists all s3 buckets -- aws_s3_bucket_info: - register: result - -- name: List buckets - debug: - msg: "{{ result['buckets'] }}" -''' - -RETURN = ''' -buckets: - description: "List of buckets" - returned: always - sample: - - creation_date: 2017-07-06 15:05:12 +00:00 - name: my_bucket - type: list -''' - -import traceback - -try: - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, HAS_BOTO3, camel_dict_to_snake_dict, - get_aws_connection_info) - - -def get_bucket_list(module, connection): - """ - Return result of list_buckets json encoded - :param module: - :param connection: - :return: - """ - try: - buckets = camel_dict_to_snake_dict(connection.list_buckets())['buckets'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - return buckets - - -def main(): - """ - Get list of S3 buckets - :return: - """ - - # Ensure we have an empty dict - result = {} - - # Including ec2 argument spec - module = AnsibleModule(argument_spec=ec2_argument_spec(), supports_check_mode=True) - is_old_facts = module._name == 'aws_s3_bucket_facts' - if is_old_facts: - module.deprecate("The 'aws_s3_bucket_facts' module has been renamed to 'aws_s3_bucket_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - # Verify Boto3 is used - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - # Set up connection - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=HAS_BOTO3) - connection = boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, - **aws_connect_params) - - # Gather results - result['buckets'] = get_bucket_list(module, connection) - - # Send exit - if is_old_facts: - module.exit_json(msg="Retrieved s3 facts.", ansible_facts=result) - else: - module.exit_json(msg="Retrieved s3 info.", **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_s3_cors.py b/lib/ansible/modules/cloud/amazon/aws_s3_cors.py deleted file mode 100644 index 451fdd9e3e..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_s3_cors.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_s3_cors -short_description: Manage CORS for S3 buckets in AWS -description: - - Manage CORS for S3 buckets in AWS -version_added: "2.5" -author: "Oyvind Saltvik (@fivethreeo)" -options: - name: - description: - - Name of the s3 bucket - required: true - type: str - rules: - description: - - Cors rules to put on the s3 bucket - type: list - state: - description: - - Create or remove cors on the s3 bucket - required: true - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create a simple cors for s3 bucket -- aws_s3_cors: - name: mys3bucket - state: present - rules: - - allowed_origins: - - http://www.example.com/ - allowed_methods: - - GET - - POST - allowed_headers: - - Authorization - expose_headers: - - x-amz-server-side-encryption - - x-amz-request-id - max_age_seconds: 30000 - -# Remove cors for s3 bucket -- aws_s3_cors: - name: mys3bucket - state: absent -''' - -RETURN = ''' -changed: - description: check to see if a change was made to the rules - returned: always - type: bool - sample: true -name: - description: name of bucket - returned: always - type: str - sample: 'bucket-name' -rules: - description: list of current rules - returned: always - type: list - sample: [ - { - "allowed_headers": [ - "Authorization" - ], - "allowed_methods": [ - "GET" - ], - "allowed_origins": [ - "*" - ], - "max_age_seconds": 30000 - } - ] -''' - -try: - from botocore.exceptions import ClientError, BotoCoreError -except Exception: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import snake_dict_to_camel_dict, compare_policies - - -def create_or_update_bucket_cors(connection, module): - - name = module.params.get("name") - rules = module.params.get("rules", []) - changed = False - - try: - current_camel_rules = connection.get_bucket_cors(Bucket=name)['CORSRules'] - except ClientError: - current_camel_rules = [] - - new_camel_rules = snake_dict_to_camel_dict(rules, capitalize_first=True) - # compare_policies() takes two dicts and makes them hashable for comparison - if compare_policies(new_camel_rules, current_camel_rules): - changed = True - - if changed: - try: - cors = connection.put_bucket_cors(Bucket=name, CORSConfiguration={'CORSRules': new_camel_rules}) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update CORS for bucket {0}".format(name)) - - module.exit_json(changed=changed, name=name, rules=rules) - - -def destroy_bucket_cors(connection, module): - - name = module.params.get("name") - changed = False - - try: - cors = connection.delete_bucket_cors(Bucket=name) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to delete CORS for bucket {0}".format(name)) - - module.exit_json(changed=changed) - - -def main(): - - argument_spec = dict( - name=dict(required=True, type='str'), - rules=dict(type='list'), - state=dict(type='str', choices=['present', 'absent'], required=True) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - - client = module.client('s3') - - state = module.params.get("state") - - if state == 'present': - create_or_update_bucket_cors(client, module) - elif state == 'absent': - destroy_bucket_cors(client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_secret.py b/lib/ansible/modules/cloud/amazon/aws_secret.py deleted file mode 100644 index 91a0bef471..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_secret.py +++ /dev/null @@ -1,404 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, REY Remi -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: aws_secret -short_description: Manage secrets stored in AWS Secrets Manager. -description: - - Create, update, and delete secrets stored in AWS Secrets Manager. -author: "REY Remi (@rrey)" -version_added: "2.8" -requirements: [ 'botocore>=1.10.0', 'boto3' ] -options: - name: - description: - - Friendly name for the secret you are creating. - required: true - type: str - state: - description: - - Whether the secret should be exist or not. - default: 'present' - choices: ['present', 'absent'] - type: str - recovery_window: - description: - - Only used if state is absent. - - Specifies the number of days that Secrets Manager waits before it can delete the secret. - - If set to 0, the deletion is forced without recovery. - default: 30 - type: int - description: - description: - - Specifies a user-provided description of the secret. - type: str - kms_key_id: - description: - - Specifies the ARN or alias of the AWS KMS customer master key (CMK) to be - used to encrypt the `secret_string` or `secret_binary` values in the versions stored in this secret. - type: str - secret_type: - description: - - Specifies the type of data that you want to encrypt. - choices: ['binary', 'string'] - default: 'string' - type: str - secret: - description: - - Specifies string or binary data that you want to encrypt and store in the new version of the secret. - default: "" - type: str - tags: - description: - - Specifies a list of user-defined tags that are attached to the secret. - type: dict - rotation_lambda: - description: - - Specifies the ARN of the Lambda function that can rotate the secret. - type: str - rotation_interval: - description: - - Specifies the number of days between automatic scheduled rotations of the secret. - default: 30 - type: int -extends_documentation_fragment: - - ec2 - - aws -''' - - -EXAMPLES = r''' -- name: Add string to AWS Secrets Manager - aws_secret: - name: 'test_secret_string' - state: present - secret_type: 'string' - secret: "{{ super_secret_string }}" - -- name: remove string from AWS Secrets Manager - aws_secret: - name: 'test_secret_string' - state: absent - secret_type: 'string' - secret: "{{ super_secret_string }}" -''' - - -RETURN = r''' -secret: - description: The secret information - returned: always - type: complex - contains: - arn: - description: The ARN of the secret - returned: always - type: str - sample: arn:aws:secretsmanager:eu-west-1:xxxxxxxxxx:secret:xxxxxxxxxxx - last_accessed_date: - description: The date the secret was last accessed - returned: always - type: str - sample: '2018-11-20T01:00:00+01:00' - last_changed_date: - description: The date the secret was last modified. - returned: always - type: str - sample: '2018-11-20T12:16:38.433000+01:00' - name: - description: The secret name. - returned: always - type: str - sample: my_secret - rotation_enabled: - description: The secret rotation status. - returned: always - type: bool - sample: false - version_ids_to_stages: - description: Provide the secret version ids and the associated secret stage. - returned: always - type: dict - sample: { "dc1ed59b-6d8e-4450-8b41-536dfe4600a9": [ "AWSCURRENT" ] } -''' - -from ansible.module_utils._text import to_bytes -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import snake_dict_to_camel_dict, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, compare_aws_tags, ansible_dict_to_boto3_tag_list - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - - -class Secret(object): - """An object representation of the Secret described by the self.module args""" - def __init__(self, name, secret_type, secret, description="", kms_key_id=None, - tags=None, lambda_arn=None, rotation_interval=None): - self.name = name - self.description = description - self.kms_key_id = kms_key_id - if secret_type == "binary": - self.secret_type = "SecretBinary" - else: - self.secret_type = "SecretString" - self.secret = secret - self.tags = tags or {} - self.rotation_enabled = False - if lambda_arn: - self.rotation_enabled = True - self.rotation_lambda_arn = lambda_arn - self.rotation_rules = {"AutomaticallyAfterDays": int(rotation_interval)} - - @property - def create_args(self): - args = { - "Name": self.name - } - if self.description: - args["Description"] = self.description - if self.kms_key_id: - args["KmsKeyId"] = self.kms_key_id - if self.tags: - args["Tags"] = ansible_dict_to_boto3_tag_list(self.tags) - args[self.secret_type] = self.secret - return args - - @property - def update_args(self): - args = { - "SecretId": self.name - } - if self.description: - args["Description"] = self.description - if self.kms_key_id: - args["KmsKeyId"] = self.kms_key_id - args[self.secret_type] = self.secret - return args - - @property - def boto3_tags(self): - return ansible_dict_to_boto3_tag_list(self.Tags) - - def as_dict(self): - result = self.__dict__ - result.pop("tags") - return snake_dict_to_camel_dict(result) - - -class SecretsManagerInterface(object): - """An interface with SecretsManager""" - - def __init__(self, module): - self.module = module - self.client = self.module.client('secretsmanager') - - def get_secret(self, name): - try: - secret = self.client.describe_secret(SecretId=name) - except self.client.exceptions.ResourceNotFoundException: - secret = None - except Exception as e: - self.module.fail_json_aws(e, msg="Failed to describe secret") - return secret - - def create_secret(self, secret): - if self.module.check_mode: - self.module.exit_json(changed=True) - try: - created_secret = self.client.create_secret(**secret.create_args) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to create secret") - - if secret.rotation_enabled: - response = self.update_rotation(secret) - created_secret["VersionId"] = response.get("VersionId") - return created_secret - - def update_secret(self, secret): - if self.module.check_mode: - self.module.exit_json(changed=True) - - try: - response = self.client.update_secret(**secret.update_args) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to update secret") - return response - - def restore_secret(self, name): - if self.module.check_mode: - self.module.exit_json(changed=True) - try: - response = self.client.restore_secret(SecretId=name) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to restore secret") - return response - - def delete_secret(self, name, recovery_window): - if self.module.check_mode: - self.module.exit_json(changed=True) - try: - if recovery_window == 0: - response = self.client.delete_secret(SecretId=name, ForceDeleteWithoutRecovery=True) - else: - response = self.client.delete_secret(SecretId=name, RecoveryWindowInDays=recovery_window) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to delete secret") - return response - - def update_rotation(self, secret): - if secret.rotation_enabled: - try: - response = self.client.rotate_secret( - SecretId=secret.name, - RotationLambdaARN=secret.rotation_lambda_arn, - RotationRules=secret.rotation_rules) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to rotate secret secret") - else: - try: - response = self.client.cancel_rotate_secret(SecretId=secret.name) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to cancel rotation") - return response - - def tag_secret(self, secret_name, tags): - try: - self.client.tag_resource(SecretId=secret_name, Tags=tags) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to add tag(s) to secret") - - def untag_secret(self, secret_name, tag_keys): - try: - self.client.untag_resource(SecretId=secret_name, TagKeys=tag_keys) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Failed to remove tag(s) from secret") - - def secrets_match(self, desired_secret, current_secret): - """Compare secrets except tags and rotation - - Args: - desired_secret: camel dict representation of the desired secret state. - current_secret: secret reference as returned by the secretsmanager api. - - Returns: bool - """ - if desired_secret.description != current_secret.get("Description", ""): - return False - if desired_secret.kms_key_id != current_secret.get("KmsKeyId"): - return False - current_secret_value = self.client.get_secret_value(SecretId=current_secret.get("Name")) - if desired_secret.secret_type == 'SecretBinary': - desired_value = to_bytes(desired_secret.secret) - else: - desired_value = desired_secret.secret - if desired_value != current_secret_value.get(desired_secret.secret_type): - return False - return True - - -def rotation_match(desired_secret, current_secret): - """Compare secrets rotation configuration - - Args: - desired_secret: camel dict representation of the desired secret state. - current_secret: secret reference as returned by the secretsmanager api. - - Returns: bool - """ - if desired_secret.rotation_enabled != current_secret.get("RotationEnabled", False): - return False - if desired_secret.rotation_enabled: - if desired_secret.rotation_lambda_arn != current_secret.get("RotationLambdaARN"): - return False - if desired_secret.rotation_rules != current_secret.get("RotationRules"): - return False - return True - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'name': dict(required=True), - 'state': dict(choices=['present', 'absent'], default='present'), - 'description': dict(default=""), - 'kms_key_id': dict(), - 'secret_type': dict(choices=['binary', 'string'], default="string"), - 'secret': dict(default=""), - 'tags': dict(type='dict', default={}), - 'rotation_lambda': dict(), - 'rotation_interval': dict(type='int', default=30), - 'recovery_window': dict(type='int', default=30), - }, - supports_check_mode=True, - ) - - changed = False - state = module.params.get('state') - secrets_mgr = SecretsManagerInterface(module) - recovery_window = module.params.get('recovery_window') - secret = Secret( - module.params.get('name'), - module.params.get('secret_type'), - module.params.get('secret'), - description=module.params.get('description'), - kms_key_id=module.params.get('kms_key_id'), - tags=module.params.get('tags'), - lambda_arn=module.params.get('rotation_lambda'), - rotation_interval=module.params.get('rotation_interval') - ) - - current_secret = secrets_mgr.get_secret(secret.name) - - if state == 'absent': - if current_secret: - if not current_secret.get("DeletedDate"): - result = camel_dict_to_snake_dict(secrets_mgr.delete_secret(secret.name, recovery_window=recovery_window)) - changed = True - elif current_secret.get("DeletedDate") and recovery_window == 0: - result = camel_dict_to_snake_dict(secrets_mgr.delete_secret(secret.name, recovery_window=recovery_window)) - changed = True - else: - result = "secret does not exist" - if state == 'present': - if current_secret is None: - result = secrets_mgr.create_secret(secret) - changed = True - else: - if current_secret.get("DeletedDate"): - secrets_mgr.restore_secret(secret.name) - changed = True - if not secrets_mgr.secrets_match(secret, current_secret): - result = secrets_mgr.update_secret(secret) - changed = True - if not rotation_match(secret, current_secret): - result = secrets_mgr.update_rotation(secret) - changed = True - current_tags = boto3_tag_list_to_ansible_dict(current_secret.get('Tags', [])) - tags_to_add, tags_to_remove = compare_aws_tags(current_tags, secret.tags) - if tags_to_add: - secrets_mgr.tag_secret(secret.name, ansible_dict_to_boto3_tag_list(tags_to_add)) - changed = True - if tags_to_remove: - secrets_mgr.untag_secret(secret.name, tags_to_remove) - changed = True - result = camel_dict_to_snake_dict(secrets_mgr.get_secret(secret.name)) - result.pop("response_metadata") - module.exit_json(changed=changed, secret=result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_ses_identity.py b/lib/ansible/modules/cloud/amazon/aws_ses_identity.py deleted file mode 100644 index 50ffef74de..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_ses_identity.py +++ /dev/null @@ -1,546 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_ses_identity -short_description: Manages SES email and domain identity -description: - - This module allows the user to manage verified email and domain identity for SES. - - This covers verifying and removing identities as well as setting up complaint, bounce - and delivery notification settings. -version_added: "2.5" -author: Ed Costello (@orthanc) - -options: - identity: - description: - - This is the email address or domain to verify / delete. - - If this contains an '@' then it will be considered an email. Otherwise it will be considered a domain. - required: true - type: str - state: - description: Whether to create(or update) or delete the identity. - default: present - choices: [ 'present', 'absent' ] - type: str - bounce_notifications: - description: - - Setup the SNS topic used to report bounce notifications. - - If omitted, bounce notifications will not be delivered to a SNS topic. - - If bounce notifications are not delivered to a SNS topic, I(feedback_forwarding) must be enabled. - suboptions: - topic: - description: - - The ARN of the topic to send notifications to. - - If omitted, notifications will not be delivered to a SNS topic. - include_headers: - description: - - Whether or not to include headers when delivering to the SNS topic. - - If I(topic) is not specified this will have no impact, but the SES setting is updated even if there is no topic. - type: bool - default: No - type: dict - complaint_notifications: - description: - - Setup the SNS topic used to report complaint notifications. - - If omitted, complaint notifications will not be delivered to a SNS topic. - - If complaint notifications are not delivered to a SNS topic, I(feedback_forwarding) must be enabled. - suboptions: - topic: - description: - - The ARN of the topic to send notifications to. - - If omitted, notifications will not be delivered to a SNS topic. - include_headers: - description: - - Whether or not to include headers when delivering to the SNS topic. - - If I(topic) is not specified this will have no impact, but the SES setting is updated even if there is no topic. - type: bool - default: No - type: dict - delivery_notifications: - description: - - Setup the SNS topic used to report delivery notifications. - - If omitted, delivery notifications will not be delivered to a SNS topic. - suboptions: - topic: - description: - - The ARN of the topic to send notifications to. - - If omitted, notifications will not be delivered to a SNS topic. - include_headers: - description: - - Whether or not to include headers when delivering to the SNS topic. - - If I(topic) is not specified this will have no impact, but the SES setting is updated even if there is no topic. - type: bool - default: No - type: dict - feedback_forwarding: - description: - - Whether or not to enable feedback forwarding. - - This can only be false if both I(bounce_notifications) and I(complaint_notifications) specify SNS topics. - type: 'bool' - default: True -requirements: [ 'botocore', 'boto3' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Ensure example@example.com email identity exists - aws_ses_identity: - identity: example@example.com - state: present - -- name: Delete example@example.com email identity - aws_ses_identity: - email: example@example.com - state: absent - -- name: Ensure example.com domain identity exists - aws_ses_identity: - identity: example.com - state: present - -# Create an SNS topic and send bounce and complaint notifications to it -# instead of emailing the identity owner -- name: Ensure complaints-topic exists - sns_topic: - name: "complaints-topic" - state: present - purge_subscriptions: False - register: topic_info - -- name: Deliver feedback to topic instead of owner email - aws_ses_identity: - identity: example@example.com - state: present - complaint_notifications: - topic: "{{ topic_info.sns_arn }}" - include_headers: True - bounce_notifications: - topic: "{{ topic_info.sns_arn }}" - include_headers: False - feedback_forwarding: False - -# Create an SNS topic for delivery notifications and leave complaints -# Being forwarded to the identity owner email -- name: Ensure delivery-notifications-topic exists - sns_topic: - name: "delivery-notifications-topic" - state: present - purge_subscriptions: False - register: topic_info - -- name: Delivery notifications to topic - aws_ses_identity: - identity: example@example.com - state: present - delivery_notifications: - topic: "{{ topic_info.sns_arn }}" -''' - -RETURN = ''' -identity: - description: The identity being modified. - returned: success - type: str - sample: example@example.com -identity_arn: - description: The arn of the identity being modified. - returned: success - type: str - sample: arn:aws:ses:us-east-1:12345678:identity/example@example.com -verification_attributes: - description: The verification information for the identity. - returned: success - type: complex - sample: { - "verification_status": "Pending", - "verification_token": "...." - } - contains: - verification_status: - description: The verification status of the identity. - type: str - sample: "Pending" - verification_token: - description: The verification token for a domain identity. - type: str -notification_attributes: - description: The notification setup for the identity. - returned: success - type: complex - sample: { - "bounce_topic": "arn:aws:sns:....", - "complaint_topic": "arn:aws:sns:....", - "delivery_topic": "arn:aws:sns:....", - "forwarding_enabled": false, - "headers_in_bounce_notifications_enabled": true, - "headers_in_complaint_notifications_enabled": true, - "headers_in_delivery_notifications_enabled": true - } - contains: - bounce_topic: - description: - - The ARN of the topic bounce notifications are delivered to. - - Omitted if bounce notifications are not delivered to a topic. - type: str - complaint_topic: - description: - - The ARN of the topic complaint notifications are delivered to. - - Omitted if complaint notifications are not delivered to a topic. - type: str - delivery_topic: - description: - - The ARN of the topic delivery notifications are delivered to. - - Omitted if delivery notifications are not delivered to a topic. - type: str - forwarding_enabled: - description: Whether or not feedback forwarding is enabled. - type: bool - headers_in_bounce_notifications_enabled: - description: Whether or not headers are included in messages delivered to the bounce topic. - type: bool - headers_in_complaint_notifications_enabled: - description: Whether or not headers are included in messages delivered to the complaint topic. - type: bool - headers_in_delivery_notifications_enabled: - description: Whether or not headers are included in messages delivered to the delivery topic. - type: bool -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, get_aws_connection_info - -import time - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def get_verification_attributes(connection, module, identity, retries=0, retryDelay=10): - # Unpredictably get_identity_verification_attributes doesn't include the identity even when we've - # just registered it. Suspect this is an eventual consistency issue on AWS side. - # Don't want this complexity exposed users of the module as they'd have to retry to ensure - # a consistent return from the module. - # To avoid this we have an internal retry that we use only after registering the identity. - for attempt in range(0, retries + 1): - try: - response = connection.get_identity_verification_attributes(Identities=[identity], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to retrieve identity verification attributes for {identity}'.format(identity=identity)) - identity_verification = response['VerificationAttributes'] - if identity in identity_verification: - break - time.sleep(retryDelay) - if identity not in identity_verification: - return None - return identity_verification[identity] - - -def get_identity_notifications(connection, module, identity, retries=0, retryDelay=10): - # Unpredictably get_identity_notifications doesn't include the notifications when we've - # just registered the identity. - # Don't want this complexity exposed users of the module as they'd have to retry to ensure - # a consistent return from the module. - # To avoid this we have an internal retry that we use only when getting the current notification - # status for return. - for attempt in range(0, retries + 1): - try: - response = connection.get_identity_notification_attributes(Identities=[identity], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to retrieve identity notification attributes for {identity}'.format(identity=identity)) - notification_attributes = response['NotificationAttributes'] - - # No clear AWS docs on when this happens, but it appears sometimes identities are not included in - # in the notification attributes when the identity is first registered. Suspect that this is caused by - # eventual consistency within the AWS services. It's been observed in builds so we need to handle it. - # - # When this occurs, just return None and we'll assume no identity notification settings have been changed - # from the default which is reasonable if this is just eventual consistency on creation. - # See: https://github.com/ansible/ansible/issues/36065 - if identity in notification_attributes: - break - else: - # Paranoia check for coding errors, we only requested one identity, so if we get a different one - # something has gone very wrong. - if len(notification_attributes) != 0: - module.fail_json( - msg='Unexpected identity found in notification attributes, expected {0} but got {1!r}.'.format( - identity, - notification_attributes.keys(), - ) - ) - time.sleep(retryDelay) - if identity not in notification_attributes: - return None - return notification_attributes[identity] - - -def desired_topic(module, notification_type): - arg_dict = module.params.get(notification_type.lower() + '_notifications') - if arg_dict: - return arg_dict.get('topic', None) - else: - return None - - -def update_notification_topic(connection, module, identity, identity_notifications, notification_type): - topic_key = notification_type + 'Topic' - if identity_notifications is None: - # If there is no configuration for notifications cannot be being sent to topics - # hence assume None as the current state. - current = None - elif topic_key in identity_notifications: - current = identity_notifications[topic_key] - else: - # If there is information on the notifications setup but no information on the - # particular notification topic it's pretty safe to assume there's no topic for - # this notification. AWS API docs suggest this information will always be - # included but best to be defensive - current = None - - required = desired_topic(module, notification_type) - - if current != required: - try: - if not module.check_mode: - connection.set_identity_notification_topic(Identity=identity, NotificationType=notification_type, SnsTopic=required, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set identity notification topic for {identity} {notification_type}'.format( - identity=identity, - notification_type=notification_type, - )) - return True - return False - - -def update_notification_topic_headers(connection, module, identity, identity_notifications, notification_type): - arg_dict = module.params.get(notification_type.lower() + '_notifications') - header_key = 'HeadersIn' + notification_type + 'NotificationsEnabled' - if identity_notifications is None: - # If there is no configuration for topic notifications, headers cannot be being - # forwarded, hence assume false. - current = False - elif header_key in identity_notifications: - current = identity_notifications[header_key] - else: - # AWS API doc indicates that the headers in fields are optional. Unfortunately - # it's not clear on what this means. But it's a pretty safe assumption that it means - # headers are not included since most API consumers would interpret absence as false. - current = False - - if arg_dict is not None and 'include_headers' in arg_dict: - required = arg_dict['include_headers'] - else: - required = False - - if current != required: - try: - if not module.check_mode: - connection.set_identity_headers_in_notifications_enabled(Identity=identity, NotificationType=notification_type, Enabled=required, - aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set identity headers in notification for {identity} {notification_type}'.format( - identity=identity, - notification_type=notification_type, - )) - return True - return False - - -def update_feedback_forwarding(connection, module, identity, identity_notifications): - if identity_notifications is None: - # AWS requires feedback forwarding to be enabled unless bounces and complaints - # are being handled by SNS topics. So in the absence of identity_notifications - # information existing feedback forwarding must be on. - current = True - elif 'ForwardingEnabled' in identity_notifications: - current = identity_notifications['ForwardingEnabled'] - else: - # If there is information on the notifications setup but no information on the - # forwarding state it's pretty safe to assume forwarding is off. AWS API docs - # suggest this information will always be included but best to be defensive - current = False - - required = module.params.get('feedback_forwarding') - - if current != required: - try: - if not module.check_mode: - connection.set_identity_feedback_forwarding_enabled(Identity=identity, ForwardingEnabled=required, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set identity feedback forwarding for {identity}'.format(identity=identity)) - return True - return False - - -def create_mock_notifications_response(module): - resp = { - "ForwardingEnabled": module.params.get('feedback_forwarding'), - } - for notification_type in ('Bounce', 'Complaint', 'Delivery'): - arg_dict = module.params.get(notification_type.lower() + '_notifications') - if arg_dict is not None and 'topic' in arg_dict: - resp[notification_type + 'Topic'] = arg_dict['topic'] - - header_key = 'HeadersIn' + notification_type + 'NotificationsEnabled' - if arg_dict is not None and 'include_headers' in arg_dict: - resp[header_key] = arg_dict['include_headers'] - else: - resp[header_key] = False - return resp - - -def update_identity_notifications(connection, module): - identity = module.params.get('identity') - changed = False - identity_notifications = get_identity_notifications(connection, module, identity) - - for notification_type in ('Bounce', 'Complaint', 'Delivery'): - changed |= update_notification_topic(connection, module, identity, identity_notifications, notification_type) - changed |= update_notification_topic_headers(connection, module, identity, identity_notifications, notification_type) - - changed |= update_feedback_forwarding(connection, module, identity, identity_notifications) - - if changed or identity_notifications is None: - if module.check_mode: - identity_notifications = create_mock_notifications_response(module) - else: - identity_notifications = get_identity_notifications(connection, module, identity, retries=4) - return changed, identity_notifications - - -def validate_params_for_identity_present(module): - if module.params.get('feedback_forwarding') is False: - if not (desired_topic(module, 'Bounce') and desired_topic(module, 'Complaint')): - module.fail_json(msg="Invalid Parameter Value 'False' for 'feedback_forwarding'. AWS requires " - "feedback forwarding to be enabled unless bounces and complaints are handled by SNS topics") - - -def create_or_update_identity(connection, module, region, account_id): - identity = module.params.get('identity') - changed = False - verification_attributes = get_verification_attributes(connection, module, identity) - if verification_attributes is None: - try: - if not module.check_mode: - if '@' in identity: - connection.verify_email_identity(EmailAddress=identity, aws_retry=True) - else: - connection.verify_domain_identity(Domain=identity, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to verify identity {identity}'.format(identity=identity)) - if module.check_mode: - verification_attributes = { - "VerificationStatus": "Pending", - } - else: - verification_attributes = get_verification_attributes(connection, module, identity, retries=4) - changed = True - elif verification_attributes['VerificationStatus'] not in ('Pending', 'Success'): - module.fail_json(msg="Identity " + identity + " in bad status " + verification_attributes['VerificationStatus'], - verification_attributes=camel_dict_to_snake_dict(verification_attributes)) - - if verification_attributes is None: - module.fail_json(msg='Unable to load identity verification attributes after registering identity.') - - notifications_changed, notification_attributes = update_identity_notifications(connection, module) - changed |= notifications_changed - - if notification_attributes is None: - module.fail_json(msg='Unable to load identity notification attributes.') - - identity_arn = 'arn:aws:ses:' + region + ':' + account_id + ':identity/' + identity - - module.exit_json( - changed=changed, - identity=identity, - identity_arn=identity_arn, - verification_attributes=camel_dict_to_snake_dict(verification_attributes), - notification_attributes=camel_dict_to_snake_dict(notification_attributes), - ) - - -def destroy_identity(connection, module): - identity = module.params.get('identity') - changed = False - verification_attributes = get_verification_attributes(connection, module, identity) - if verification_attributes is not None: - try: - if not module.check_mode: - connection.delete_identity(Identity=identity, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to delete identity {identity}'.format(identity=identity)) - changed = True - - module.exit_json( - changed=changed, - identity=identity, - ) - - -def get_account_id(module): - sts = module.client('sts') - try: - caller_identity = sts.get_caller_identity() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to retrieve caller identity') - return caller_identity['Account'] - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - "identity": dict(required=True, type='str'), - "state": dict(default='present', choices=['present', 'absent']), - "bounce_notifications": dict(type='dict'), - "complaint_notifications": dict(type='dict'), - "delivery_notifications": dict(type='dict'), - "feedback_forwarding": dict(default=True, type='bool'), - }, - supports_check_mode=True, - ) - - for notification_type in ('bounce', 'complaint', 'delivery'): - param_name = notification_type + '_notifications' - arg_dict = module.params.get(param_name) - if arg_dict: - extra_keys = [x for x in arg_dict.keys() if x not in ('topic', 'include_headers')] - if extra_keys: - module.fail_json(msg='Unexpected keys ' + str(extra_keys) + ' in ' + param_name + ' valid keys are topic or include_headers') - - # SES APIs seem to have a much lower throttling threshold than most of the rest of the AWS APIs. - # Docs say 1 call per second. This shouldn't actually be a big problem for normal usage, but - # the ansible build runs multiple instances of the test in parallel that's caused throttling - # failures so apply a jittered backoff to call SES calls. - connection = module.client('ses', retry_decorator=AWSRetry.jittered_backoff()) - - state = module.params.get("state") - - if state == 'present': - region = get_aws_connection_info(module, boto3=True)[0] - account_id = get_account_id(module) - validate_params_for_identity_present(module) - create_or_update_identity(connection, module, region, account_id) - else: - destroy_identity(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_ses_identity_policy.py b/lib/ansible/modules/cloud/amazon/aws_ses_identity_policy.py deleted file mode 100644 index 49e950e71c..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_ses_identity_policy.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_ses_identity_policy -short_description: Manages SES sending authorization policies -description: - - This module allows the user to manage sending authorization policies associated with an SES identity (email or domain). - - SES authorization sending policies can be used to control what actors are able to send email - on behalf of the validated identity and what conditions must be met by the sent emails. -version_added: "2.6" -author: Ed Costello (@orthanc) - -options: - identity: - description: | - The SES identity to attach or remove a policy from. This can be either the full ARN or just - the verified email or domain. - required: true - type: str - policy_name: - description: The name used to identify the policy within the scope of the identity it's attached to. - required: true - type: str - policy: - description: A properly formatted JSON sending authorization policy. Required when I(state=present). - type: json - state: - description: Whether to create(or update) or delete the authorization policy on the identity. - default: present - choices: [ 'present', 'absent' ] - type: str -requirements: [ 'botocore', 'boto3' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: add sending authorization policy to domain identity - aws_ses_identity_policy: - identity: example.com - policy_name: ExamplePolicy - policy: "{{ lookup('template', 'policy.json.j2') }}" - state: present - -- name: add sending authorization policy to email identity - aws_ses_identity_policy: - identity: example@example.com - policy_name: ExamplePolicy - policy: "{{ lookup('template', 'policy.json.j2') }}" - state: present - -- name: add sending authorization policy to identity using ARN - aws_ses_identity_policy: - identity: "arn:aws:ses:us-east-1:12345678:identity/example.com" - policy_name: ExamplePolicy - policy: "{{ lookup('template', 'policy.json.j2') }}" - state: present - -- name: remove sending authorization policy - aws_ses_identity_policy: - identity: example.com - policy_name: ExamplePolicy - state: absent -''' - -RETURN = ''' -policies: - description: A list of all policies present on the identity after the operation. - returned: success - type: list - sample: [ExamplePolicy] -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import compare_policies, AWSRetry - -import json - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def get_identity_policy(connection, module, identity, policy_name): - try: - response = connection.get_identity_policies(Identity=identity, PolicyNames=[policy_name], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to retrieve identity policy {policy}'.format(policy=policy_name)) - policies = response['Policies'] - if policy_name in policies: - return policies[policy_name] - return None - - -def create_or_update_identity_policy(connection, module): - identity = module.params.get('identity') - policy_name = module.params.get('policy_name') - required_policy = module.params.get('policy') - required_policy_dict = json.loads(required_policy) - - changed = False - policy = get_identity_policy(connection, module, identity, policy_name) - policy_dict = json.loads(policy) if policy else None - if compare_policies(policy_dict, required_policy_dict): - changed = True - try: - if not module.check_mode: - connection.put_identity_policy(Identity=identity, PolicyName=policy_name, Policy=required_policy, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to put identity policy {policy}'.format(policy=policy_name)) - - # Load the list of applied policies to include in the response. - # In principle we should be able to just return the response, but given - # eventual consistency behaviours in AWS it's plausible that we could - # end up with a list that doesn't contain the policy we just added. - # So out of paranoia check for this case and if we're missing the policy - # just make sure it's present. - # - # As a nice side benefit this also means the return is correct in check mode - try: - policies_present = connection.list_identity_policies(Identity=identity, aws_retry=True)['PolicyNames'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to list identity policies') - if policy_name is not None and policy_name not in policies_present: - policies_present = list(policies_present) - policies_present.append(policy_name) - module.exit_json( - changed=changed, - policies=policies_present, - ) - - -def delete_identity_policy(connection, module): - identity = module.params.get('identity') - policy_name = module.params.get('policy_name') - - changed = False - try: - policies_present = connection.list_identity_policies(Identity=identity, aws_retry=True)['PolicyNames'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to list identity policies') - if policy_name in policies_present: - try: - if not module.check_mode: - connection.delete_identity_policy(Identity=identity, PolicyName=policy_name, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to delete identity policy {policy}'.format(policy=policy_name)) - changed = True - policies_present = list(policies_present) - policies_present.remove(policy_name) - - module.exit_json( - changed=changed, - policies=policies_present, - ) - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'identity': dict(required=True, type='str'), - 'state': dict(default='present', choices=['present', 'absent']), - 'policy_name': dict(required=True, type='str'), - 'policy': dict(type='json', default=None), - }, - required_if=[['state', 'present', ['policy']]], - supports_check_mode=True, - ) - - # SES APIs seem to have a much lower throttling threshold than most of the rest of the AWS APIs. - # Docs say 1 call per second. This shouldn't actually be a big problem for normal usage, but - # the ansible build runs multiple instances of the test in parallel that's caused throttling - # failures so apply a jittered backoff to call SES calls. - connection = module.client('ses', retry_decorator=AWSRetry.jittered_backoff()) - - state = module.params.get("state") - - if state == 'present': - create_or_update_identity_policy(connection, module) - else: - delete_identity_policy(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_ses_rule_set.py b/lib/ansible/modules/cloud/amazon/aws_ses_rule_set.py deleted file mode 100644 index a22a4136f6..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_ses_rule_set.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017, Ben Tomasik <ben@tomasik.io> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: aws_ses_rule_set -short_description: Manages SES inbound receipt rule sets -description: - - The M(aws_ses_rule_set) module allows you to create, delete, and manage SES receipt rule sets -version_added: 2.8 -author: - - "Ben Tomasik (@tomislacker)" - - "Ed Costello (@orthanc)" -requirements: [ boto3, botocore ] -options: - name: - description: - - The name of the receipt rule set. - required: True - type: str - state: - description: - - Whether to create (or update) or destroy the receipt rule set. - required: False - default: present - choices: ["absent", "present"] - type: str - active: - description: - - Whether or not this rule set should be the active rule set. Only has an impact if I(state) is C(present). - - If omitted, the active rule set will not be changed. - - If C(True) then this rule set will be made active and all others inactive. - - if C(False) then this rule set will be deactivated. Be careful with this as you can end up with no active rule set. - type: bool - required: False - force: - description: - - When deleting a rule set, deactivate it first (AWS prevents deletion of the active rule set). - type: bool - required: False - default: False -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. ---- -- name: Create default rule set and activate it if not already - aws_ses_rule_set: - name: default-rule-set - state: present - active: yes - -- name: Create some arbitrary rule set but do not activate it - aws_ses_rule_set: - name: arbitrary-rule-set - state: present - -- name: Explicitly deactivate the default rule set leaving no active rule set - aws_ses_rule_set: - name: default-rule-set - state: present - active: no - -- name: Remove an arbitrary inactive rule set - aws_ses_rule_set: - name: arbitrary-rule-set - state: absent - -- name: Remove an ruleset even if we have to first deactivate it to remove it - aws_ses_rule_set: - name: default-rule-set - state: absent - force: yes -""" - -RETURN = """ -active: - description: if the SES rule set is active - returned: success if I(state) is C(present) - type: bool - sample: true -rule_sets: - description: The list of SES receipt rule sets that exist after any changes. - returned: success - type: list - sample: [{ - "created_timestamp": "2018-02-25T01:20:32.690000+00:00", - "name": "default-rule-set" - }] -""" - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAWSModule - - -def list_rule_sets(client, module): - try: - response = client.list_receipt_rule_sets(aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't list rule sets.") - return response['RuleSets'] - - -def rule_set_in(name, rule_sets): - return any([s for s in rule_sets if s['Name'] == name]) - - -def ruleset_active(client, module, name): - try: - active_rule_set = client.describe_active_receipt_rule_set(aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't get the active rule set.") - if active_rule_set is not None and 'Metadata' in active_rule_set: - return name == active_rule_set['Metadata']['Name'] - else: - # Metadata was not set meaning there is no active rule set - return False - - -def deactivate_rule_set(client, module): - try: - # No ruleset name deactivates all rulesets - client.set_active_receipt_rule_set(aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't set active rule set to None.") - - -def update_active_rule_set(client, module, name, desired_active): - check_mode = module.check_mode - - active = ruleset_active(client, module, name) - - changed = False - if desired_active is not None: - if desired_active and not active: - if not check_mode: - try: - client.set_active_receipt_rule_set(RuleSetName=name, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't set active rule set to {0}.".format(name)) - changed = True - active = True - elif not desired_active and active: - if not check_mode: - deactivate_rule_set(client, module) - changed = True - active = False - return changed, active - - -def create_or_update_rule_set(client, module): - name = module.params.get('name') - check_mode = module.check_mode - changed = False - - rule_sets = list_rule_sets(client, module) - if not rule_set_in(name, rule_sets): - if not check_mode: - try: - client.create_receipt_rule_set(RuleSetName=name, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't create rule set {0}.".format(name)) - changed = True - rule_sets = list(rule_sets) - rule_sets.append({ - 'Name': name, - }) - - (active_changed, active) = update_active_rule_set(client, module, name, module.params.get('active')) - changed |= active_changed - - module.exit_json( - changed=changed, - active=active, - rule_sets=[camel_dict_to_snake_dict(x) for x in rule_sets], - ) - - -def remove_rule_set(client, module): - name = module.params.get('name') - check_mode = module.check_mode - changed = False - - rule_sets = list_rule_sets(client, module) - if rule_set_in(name, rule_sets): - active = ruleset_active(client, module, name) - if active and not module.params.get('force'): - module.fail_json( - msg="Couldn't delete rule set {0} because it is currently active. Set force=true to delete an active ruleset.".format(name), - error={ - "code": "CannotDelete", - "message": "Cannot delete active rule set: {0}".format(name), - } - ) - if not check_mode: - if active and module.params.get('force'): - deactivate_rule_set(client, module) - try: - client.delete_receipt_rule_set(RuleSetName=name, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't delete rule set {0}.".format(name)) - changed = True - rule_sets = [x for x in rule_sets if x['Name'] != name] - - module.exit_json( - changed=changed, - rule_sets=[camel_dict_to_snake_dict(x) for x in rule_sets], - ) - - -def main(): - argument_spec = dict( - name=dict(type='str', required=True), - state=dict(type='str', default='present', choices=['present', 'absent']), - active=dict(type='bool'), - force=dict(type='bool', default=False), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - state = module.params.get('state') - - # SES APIs seem to have a much lower throttling threshold than most of the rest of the AWS APIs. - # Docs say 1 call per second. This shouldn't actually be a big problem for normal usage, but - # the ansible build runs multiple instances of the test in parallel that's caused throttling - # failures so apply a jittered backoff to call SES calls. - client = module.client('ses', retry_decorator=AWSRetry.jittered_backoff()) - - if state == 'absent': - remove_rule_set(client, module) - else: - create_or_update_rule_set(client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_sgw_info.py b/lib/ansible/modules/cloud/amazon/aws_sgw_info.py deleted file mode 100644 index fdb0a6dede..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_sgw_info.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2018, Loic BLOT (@nerzhul) <loic.blot@unix-experience.fr> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# This module is sponsored by E.T.A.I. (www.etai.fr) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: aws_sgw_info -short_description: Fetch AWS Storage Gateway information -description: - - Fetch AWS Storage Gateway information - - This module was called C(aws_sgw_facts) before Ansible 2.9. The usage did not change. -version_added: "2.6" -requirements: [ boto3 ] -author: Loic Blot (@nerzhul) <loic.blot@unix-experience.fr> -options: - gather_local_disks: - description: - - Gather local disks attached to the storage gateway. - type: bool - required: false - default: true - gather_tapes: - description: - - Gather tape information for storage gateways in tape mode. - type: bool - required: false - default: true - gather_file_shares: - description: - - Gather file share information for storage gateways in s3 mode. - type: bool - required: false - default: true - gather_volumes: - description: - - Gather volume information for storage gateways in iSCSI (cached & stored) modes. - type: bool - required: false - default: true -extends_documentation_fragment: - - aws - - ec2 -''' - -RETURN = ''' -gateways: - description: list of gateway objects - returned: always - type: complex - contains: - gateway_arn: - description: "Storage Gateway ARN" - returned: always - type: str - sample: "arn:aws:storagegateway:eu-west-1:367709993819:gateway/sgw-9999F888" - gateway_id: - description: "Storage Gateway ID" - returned: always - type: str - sample: "sgw-9999F888" - gateway_name: - description: "Storage Gateway friendly name" - returned: always - type: str - sample: "my-sgw-01" - gateway_operational_state: - description: "Storage Gateway operational state" - returned: always - type: str - sample: "ACTIVE" - gateway_type: - description: "Storage Gateway type" - returned: always - type: str - sample: "FILE_S3" - file_shares: - description: "Storage gateway file shares" - returned: when gateway_type == "FILE_S3" - type: complex - contains: - file_share_arn: - description: "File share ARN" - returned: always - type: str - sample: "arn:aws:storagegateway:eu-west-1:399805793479:share/share-AF999C88" - file_share_id: - description: "File share ID" - returned: always - type: str - sample: "share-AF999C88" - file_share_status: - description: "File share status" - returned: always - type: str - sample: "AVAILABLE" - tapes: - description: "Storage Gateway tapes" - returned: when gateway_type == "VTL" - type: complex - contains: - tape_arn: - description: "Tape ARN" - returned: always - type: str - sample: "arn:aws:storagegateway:eu-west-1:399805793479:tape/tape-AF999C88" - tape_barcode: - description: "Tape ARN" - returned: always - type: str - sample: "tape-AF999C88" - tape_size_in_bytes: - description: "Tape ARN" - returned: always - type: int - sample: 555887569 - tape_status: - description: "Tape ARN" - returned: always - type: str - sample: "AVAILABLE" - local_disks: - description: "Storage gateway local disks" - returned: always - type: complex - contains: - disk_allocation_type: - description: "Disk allocation type" - returned: always - type: str - sample: "CACHE STORAGE" - disk_id: - description: "Disk ID on the system" - returned: always - type: str - sample: "pci-0000:00:1f.0" - disk_node: - description: "Disk parent block device" - returned: always - type: str - sample: "/dev/sdb" - disk_path: - description: "Disk path used for the cache" - returned: always - type: str - sample: "/dev/nvme1n1" - disk_size_in_bytes: - description: "Disk size in bytes" - returned: always - type: int - sample: 107374182400 - disk_status: - description: "Disk status" - returned: always - type: str - sample: "present" -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: "Get AWS storage gateway information" - aws_sgw_info: - -- name: "Get AWS storage gateway information for region eu-west-3" - aws_sgw_info: - region: eu-west-3 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -class SGWInformationManager(object): - def __init__(self, client, module): - self.client = client - self.module = module - self.name = self.module.params.get('name') - - def fetch(self): - gateways = self.list_gateways() - for gateway in gateways: - if self.module.params.get('gather_local_disks'): - self.list_local_disks(gateway) - # File share gateway - if gateway["gateway_type"] == "FILE_S3" and self.module.params.get('gather_file_shares'): - self.list_gateway_file_shares(gateway) - # Volume tape gateway - elif gateway["gateway_type"] == "VTL" and self.module.params.get('gather_tapes'): - self.list_gateway_vtl(gateway) - # iSCSI gateway - elif gateway["gateway_type"] in ["CACHED", "STORED"] and self.module.params.get('gather_volumes'): - self.list_gateway_volumes(gateway) - - self.module.exit_json(gateways=gateways) - - """ - List all storage gateways for the AWS endpoint. - """ - def list_gateways(self): - try: - paginator = self.client.get_paginator('list_gateways') - response = paginator.paginate( - PaginationConfig={ - 'PageSize': 100, - } - ).build_full_result() - - gateways = [] - for gw in response["Gateways"]: - gateways.append(camel_dict_to_snake_dict(gw)) - - return gateways - - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Couldn't list storage gateways") - - """ - Read file share objects from AWS API response. - Drop the gateway_arn attribute from response, as it will be duplicate with parent object. - """ - @staticmethod - def _read_gateway_fileshare_response(fileshares, aws_reponse): - for share in aws_reponse["FileShareInfoList"]: - share_obj = camel_dict_to_snake_dict(share) - if "gateway_arn" in share_obj: - del share_obj["gateway_arn"] - fileshares.append(share_obj) - - return aws_reponse["NextMarker"] if "NextMarker" in aws_reponse else None - - """ - List file shares attached to AWS storage gateway when in S3 mode. - """ - def list_gateway_file_shares(self, gateway): - try: - response = self.client.list_file_shares( - GatewayARN=gateway["gateway_arn"], - Limit=100 - ) - - gateway["file_shares"] = [] - marker = self._read_gateway_fileshare_response(gateway["file_shares"], response) - - while marker is not None: - response = self.client.list_file_shares( - GatewayARN=gateway["gateway_arn"], - Marker=marker, - Limit=100 - ) - - marker = self._read_gateway_fileshare_response(gateway["file_shares"], response) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Couldn't list gateway file shares") - - """ - List storage gateway local disks - """ - def list_local_disks(self, gateway): - try: - gateway['local_disks'] = [camel_dict_to_snake_dict(disk) for disk in - self.client.list_local_disks(GatewayARN=gateway["gateway_arn"])['Disks']] - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Couldn't list storage gateway local disks") - - """ - Read tape objects from AWS API response. - Drop the gateway_arn attribute from response, as it will be duplicate with parent object. - """ - @staticmethod - def _read_gateway_tape_response(tapes, aws_response): - for tape in aws_response["TapeInfos"]: - tape_obj = camel_dict_to_snake_dict(tape) - if "gateway_arn" in tape_obj: - del tape_obj["gateway_arn"] - tapes.append(tape_obj) - - return aws_response["Marker"] if "Marker" in aws_response else None - - """ - List VTL & VTS attached to AWS storage gateway in VTL mode - """ - def list_gateway_vtl(self, gateway): - try: - response = self.client.list_tapes( - Limit=100 - ) - - gateway["tapes"] = [] - marker = self._read_gateway_tape_response(gateway["tapes"], response) - - while marker is not None: - response = self.client.list_tapes( - Marker=marker, - Limit=100 - ) - - marker = self._read_gateway_tape_response(gateway["tapes"], response) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Couldn't list storage gateway tapes") - - """ - List volumes attached to AWS storage gateway in CACHED or STORAGE mode - """ - def list_gateway_volumes(self, gateway): - try: - paginator = self.client.get_paginator('list_volumes') - response = paginator.paginate( - GatewayARN=gateway["gateway_arn"], - PaginationConfig={ - 'PageSize': 100, - } - ).build_full_result() - - gateway["volumes"] = [] - for volume in response["VolumeInfos"]: - volume_obj = camel_dict_to_snake_dict(volume) - if "gateway_arn" in volume_obj: - del volume_obj["gateway_arn"] - if "gateway_id" in volume_obj: - del volume_obj["gateway_id"] - - gateway["volumes"].append(volume_obj) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Couldn't list storage gateway volumes") - - -def main(): - argument_spec = dict( - gather_local_disks=dict(type='bool', default=True), - gather_tapes=dict(type='bool', default=True), - gather_file_shares=dict(type='bool', default=True), - gather_volumes=dict(type='bool', default=True) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - if module._name == 'aws_sgw_facts': - module.deprecate("The 'aws_sgw_facts' module has been renamed to 'aws_sgw_info'", version='2.13') - client = module.client('storagegateway') - - if client is None: # this should never happen - module.fail_json(msg='Unknown error, failed to create storagegateway client, no information from boto.') - - SGWInformationManager(client, module).fetch() - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py b/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py deleted file mode 100644 index e63071d094..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: aws_ssm_parameter_store -short_description: Manage key-value pairs in aws parameter store. -description: - - Manage key-value pairs in aws parameter store. -version_added: "2.5" -options: - name: - description: - - Parameter key name. - required: true - type: str - description: - description: - - Parameter key description. - required: false - type: str - value: - description: - - Parameter value. - required: false - type: str - state: - description: - - Creates or modifies an existing parameter. - - Deletes a parameter. - required: false - choices: ['present', 'absent'] - default: present - type: str - string_type: - description: - - Parameter String type. - required: false - choices: ['String', 'StringList', 'SecureString'] - default: String - type: str - decryption: - description: - - Work with SecureString type to get plain text secrets - type: bool - required: false - default: true - key_id: - description: - - AWS KMS key to decrypt the secrets. - - The default key (C(alias/aws/ssm)) is automatically generated the first - time it's requested. - required: false - default: alias/aws/ssm - type: str - overwrite_value: - description: - - Option to overwrite an existing value if it already exists. - required: false - version_added: "2.6" - choices: ['never', 'changed', 'always'] - default: changed - type: str -author: - - Nathan Webster (@nathanwebsterdotme) - - Bill Wang (@ozbillwang) <ozbillwang@gmail.com> - - Michael De La Rue (@mikedlr) -extends_documentation_fragment: - - aws - - ec2 -requirements: [ botocore, boto3 ] -''' - -EXAMPLES = ''' -- name: Create or update key/value pair in aws parameter store - aws_ssm_parameter_store: - name: "Hello" - description: "This is your first key" - value: "World" - -- name: Delete the key - aws_ssm_parameter_store: - name: "Hello" - state: absent - -- name: Create or update secure key/value pair with default kms key (aws/ssm) - aws_ssm_parameter_store: - name: "Hello" - description: "This is your first key" - string_type: "SecureString" - value: "World" - -- name: Create or update secure key/value pair with nominated kms key - aws_ssm_parameter_store: - name: "Hello" - description: "This is your first key" - string_type: "SecureString" - key_id: "alias/demo" - value: "World" - -- name: Always update a parameter store value and create a new version - aws_ssm_parameter_store: - name: "overwrite_example" - description: "This example will always overwrite the value" - string_type: "String" - value: "Test1234" - overwrite_value: "always" - -- name: recommend to use with aws_ssm lookup plugin - debug: msg="{{ lookup('aws_ssm', 'hello') }}" -''' - -RETURN = ''' -put_parameter: - description: Add one or more parameters to the system. - returned: success - type: dict -delete_parameter: - description: Delete a parameter from the system. - returned: success - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # Handled by AnsibleAWSModule - - -def update_parameter(client, module, args): - changed = False - response = {} - - try: - response = client.put_parameter(**args) - changed = True - except ClientError as e: - module.fail_json_aws(e, msg="setting parameter") - - return changed, response - - -def create_update_parameter(client, module): - changed = False - existing_parameter = None - response = {} - - args = dict( - Name=module.params.get('name'), - Value=module.params.get('value'), - Type=module.params.get('string_type') - ) - - if (module.params.get('overwrite_value') in ("always", "changed")): - args.update(Overwrite=True) - else: - args.update(Overwrite=False) - - if module.params.get('description'): - args.update(Description=module.params.get('description')) - - if module.params.get('string_type') == 'SecureString': - args.update(KeyId=module.params.get('key_id')) - - try: - existing_parameter = client.get_parameter(Name=args['Name'], WithDecryption=True) - except Exception: - pass - - if existing_parameter: - if (module.params.get('overwrite_value') == 'always'): - - (changed, response) = update_parameter(client, module, args) - - elif (module.params.get('overwrite_value') == 'changed'): - if existing_parameter['Parameter']['Type'] != args['Type']: - (changed, response) = update_parameter(client, module, args) - - if existing_parameter['Parameter']['Value'] != args['Value']: - (changed, response) = update_parameter(client, module, args) - - if args.get('Description'): - # Description field not available from get_parameter function so get it from describe_parameters - describe_existing_parameter = None - try: - describe_existing_parameter_paginator = client.get_paginator('describe_parameters') - describe_existing_parameter = describe_existing_parameter_paginator.paginate( - Filters=[{"Key": "Name", "Values": [args['Name']]}]).build_full_result() - - except ClientError as e: - module.fail_json_aws(e, msg="getting description value") - - if describe_existing_parameter['Parameters'][0]['Description'] != args['Description']: - (changed, response) = update_parameter(client, module, args) - else: - (changed, response) = update_parameter(client, module, args) - - return changed, response - - -def delete_parameter(client, module): - response = {} - - try: - response = client.delete_parameter( - Name=module.params.get('name') - ) - except ClientError as e: - if e.response['Error']['Code'] == 'ParameterNotFound': - return False, {} - module.fail_json_aws(e, msg="deleting parameter") - - return True, response - - -def setup_client(module): - connection = module.client('ssm') - return connection - - -def setup_module_object(): - argument_spec = dict( - name=dict(required=True), - description=dict(), - value=dict(required=False, no_log=True), - state=dict(default='present', choices=['present', 'absent']), - string_type=dict(default='String', choices=['String', 'StringList', 'SecureString']), - decryption=dict(default=True, type='bool'), - key_id=dict(default="alias/aws/ssm"), - overwrite_value=dict(default='changed', choices=['never', 'changed', 'always']), - ) - - return AnsibleAWSModule( - argument_spec=argument_spec, - ) - - -def main(): - module = setup_module_object() - state = module.params.get('state') - client = setup_client(module) - - invocations = { - "present": create_update_parameter, - "absent": delete_parameter, - } - (changed, response) = invocations[state](client, module) - module.exit_json(changed=changed, response=response) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine.py b/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine.py deleted file mode 100644 index 329ee4283d..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2019, Tom De Keyser (@tdekeyser) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) - -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_step_functions_state_machine - -short_description: Manage AWS Step Functions state machines - -version_added: "2.10" - -description: - - Create, update and delete state machines in AWS Step Functions. - - Calling the module in C(state=present) for an existing AWS Step Functions state machine - will attempt to update the state machine definition, IAM Role, or tags with the provided data. - -options: - name: - description: - - Name of the state machine - required: true - type: str - definition: - description: - - The Amazon States Language definition of the state machine. See - U(https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html) for more - information on the Amazon States Language. - - "This parameter is required when C(state=present)." - type: json - role_arn: - description: - - The ARN of the IAM Role that will be used by the state machine for its executions. - - "This parameter is required when C(state=present)." - type: str - state: - description: - - Desired state for the state machine - default: present - choices: [ present, absent ] - type: str - tags: - description: - - A hash/dictionary of tags to add to the new state machine or to add/remove from an existing one. - type: dict - purge_tags: - description: - - If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. - If the I(tags) parameter is not set then tags will not be modified. - default: yes - type: bool - -extends_documentation_fragment: - - aws - - ec2 - -author: - - Tom De Keyser (@tdekeyser) -''' - -EXAMPLES = ''' -# Create a new AWS Step Functions state machine -- name: Setup HelloWorld state machine - aws_step_functions_state_machine: - name: "HelloWorldStateMachine" - definition: "{{ lookup('file','state_machine.json') }}" - role_arn: arn:aws:iam::987654321012:role/service-role/invokeLambdaStepFunctionsRole - tags: - project: helloWorld - -# Update an existing state machine -- name: Change IAM Role and tags of HelloWorld state machine - aws_step_functions_state_machine: - name: HelloWorldStateMachine - definition: "{{ lookup('file','state_machine.json') }}" - role_arn: arn:aws:iam::987654321012:role/service-role/anotherStepFunctionsRole - tags: - otherTag: aDifferentTag - -# Remove the AWS Step Functions state machine -- name: Delete HelloWorld state machine - aws_step_functions_state_machine: - name: HelloWorldStateMachine - state: absent -''' - -RETURN = ''' -state_machine_arn: - description: ARN of the AWS Step Functions state machine - type: str - returned: always -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, AWSRetry, compare_aws_tags, boto3_tag_list_to_ansible_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - - -def manage_state_machine(state, sfn_client, module): - state_machine_arn = get_state_machine_arn(sfn_client, module) - - if state == 'present': - if state_machine_arn is None: - create(sfn_client, module) - else: - update(state_machine_arn, sfn_client, module) - elif state == 'absent': - if state_machine_arn is not None: - remove(state_machine_arn, sfn_client, module) - - check_mode(module, msg='State is up-to-date.') - module.exit_json(changed=False) - - -def create(sfn_client, module): - check_mode(module, msg='State machine would be created.', changed=True) - - tags = module.params.get('tags') - sfn_tags = ansible_dict_to_boto3_tag_list(tags, tag_name_key_name='key', tag_value_key_name='value') if tags else [] - - state_machine = sfn_client.create_state_machine( - name=module.params.get('name'), - definition=module.params.get('definition'), - roleArn=module.params.get('role_arn'), - tags=sfn_tags - ) - module.exit_json(changed=True, state_machine_arn=state_machine.get('stateMachineArn')) - - -def remove(state_machine_arn, sfn_client, module): - check_mode(module, msg='State machine would be deleted: {0}'.format(state_machine_arn), changed=True) - - sfn_client.delete_state_machine(stateMachineArn=state_machine_arn) - module.exit_json(changed=True, state_machine_arn=state_machine_arn) - - -def update(state_machine_arn, sfn_client, module): - tags_to_add, tags_to_remove = compare_tags(state_machine_arn, sfn_client, module) - - if params_changed(state_machine_arn, sfn_client, module) or tags_to_add or tags_to_remove: - check_mode(module, msg='State machine would be updated: {0}'.format(state_machine_arn), changed=True) - - sfn_client.update_state_machine( - stateMachineArn=state_machine_arn, - definition=module.params.get('definition'), - roleArn=module.params.get('role_arn') - ) - sfn_client.untag_resource( - resourceArn=state_machine_arn, - tagKeys=tags_to_remove - ) - sfn_client.tag_resource( - resourceArn=state_machine_arn, - tags=ansible_dict_to_boto3_tag_list(tags_to_add, tag_name_key_name='key', tag_value_key_name='value') - ) - - module.exit_json(changed=True, state_machine_arn=state_machine_arn) - - -def compare_tags(state_machine_arn, sfn_client, module): - new_tags = module.params.get('tags') - current_tags = sfn_client.list_tags_for_resource(resourceArn=state_machine_arn).get('tags') - return compare_aws_tags(boto3_tag_list_to_ansible_dict(current_tags), new_tags if new_tags else {}, module.params.get('purge_tags')) - - -def params_changed(state_machine_arn, sfn_client, module): - """ - Check whether the state machine definition or IAM Role ARN is different - from the existing state machine parameters. - """ - current = sfn_client.describe_state_machine(stateMachineArn=state_machine_arn) - return current.get('definition') != module.params.get('definition') or current.get('roleArn') != module.params.get('role_arn') - - -def get_state_machine_arn(sfn_client, module): - """ - Finds the state machine ARN based on the name parameter. Returns None if - there is no state machine with this name. - """ - target_name = module.params.get('name') - all_state_machines = sfn_client.list_state_machines(aws_retry=True).get('stateMachines') - - for state_machine in all_state_machines: - if state_machine.get('name') == target_name: - return state_machine.get('stateMachineArn') - - -def check_mode(module, msg='', changed=False): - if module.check_mode: - module.exit_json(changed=changed, output=msg) - - -def main(): - module_args = dict( - name=dict(type='str', required=True), - definition=dict(type='json'), - role_arn=dict(type='str'), - state=dict(choices=['present', 'absent'], default='present'), - tags=dict(default=None, type='dict'), - purge_tags=dict(default=True, type='bool'), - ) - module = AnsibleAWSModule( - argument_spec=module_args, - required_if=[('state', 'present', ['role_arn']), ('state', 'present', ['definition'])], - supports_check_mode=True - ) - - sfn_client = module.client('stepfunctions', retry_decorator=AWSRetry.jittered_backoff(retries=5)) - state = module.params.get('state') - - try: - manage_state_machine(state, sfn_client, module) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to manage state machine') - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py b/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py deleted file mode 100644 index a6e0d7182d..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2019, Prasad Katti (@prasadkatti) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) - -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_step_functions_state_machine_execution - -short_description: Start or stop execution of an AWS Step Functions state machine. - -version_added: "2.10" - -description: - - Start or stop execution of a state machine in AWS Step Functions. - -options: - action: - description: Desired action (start or stop) for a state machine execution. - default: start - choices: [ start, stop ] - type: str - name: - description: Name of the execution. - type: str - execution_input: - description: The JSON input data for the execution. - type: json - default: {} - state_machine_arn: - description: The ARN of the state machine that will be executed. - type: str - execution_arn: - description: The ARN of the execution you wish to stop. - type: str - cause: - description: A detailed explanation of the cause for stopping the execution. - type: str - default: '' - error: - description: The error code of the failure to pass in when stopping the execution. - type: str - default: '' - -extends_documentation_fragment: - - aws - - ec2 - -author: - - Prasad Katti (@prasadkatti) -''' - -EXAMPLES = ''' -- name: Start an execution of a state machine - aws_step_functions_state_machine_execution: - name: an_execution_name - execution_input: '{ "IsHelloWorldExample": true }' - state_machine_arn: "arn:aws:states:us-west-2:682285639423:stateMachine:HelloWorldStateMachine" - -- name: Stop an execution of a state machine - aws_step_functions_state_machine_execution: - action: stop - execution_arn: "arn:aws:states:us-west-2:682285639423:execution:HelloWorldStateMachineCopy:a1e8e2b5-5dfe-d40e-d9e3-6201061047c8" - cause: "cause of task failure" - error: "error code of the failure" -''' - -RETURN = ''' -execution_arn: - description: ARN of the AWS Step Functions state machine execution. - type: str - returned: if action == start and changed == True - sample: "arn:aws:states:us-west-2:682285639423:execution:HelloWorldStateMachineCopy:a1e8e2b5-5dfe-d40e-d9e3-6201061047c8" -start_date: - description: The date the execution is started. - type: str - returned: if action == start and changed == True - sample: "2019-11-02T22:39:49.071000-07:00" -stop_date: - description: The date the execution is stopped. - type: str - returned: if action == stop - sample: "2019-11-02T22:39:49.071000-07:00" -''' - - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - - -def start_execution(module, sfn_client): - ''' - start_execution uses execution name to determine if a previous execution already exists. - If an execution by the provided name exists, call client.start_execution will not be called. - ''' - - state_machine_arn = module.params.get('state_machine_arn') - name = module.params.get('name') - execution_input = module.params.get('execution_input') - - try: - # list_executions is eventually consistent - page_iterators = sfn_client.get_paginator('list_executions').paginate(stateMachineArn=state_machine_arn) - - for execution in page_iterators.build_full_result()['executions']: - if name == execution['name']: - check_mode(module, msg='State machine execution already exists.', changed=False) - module.exit_json(changed=False) - - check_mode(module, msg='State machine execution would be started.', changed=True) - res_execution = sfn_client.start_execution( - stateMachineArn=state_machine_arn, - name=name, - input=execution_input - ) - except (ClientError, BotoCoreError) as e: - if e.response['Error']['Code'] == 'ExecutionAlreadyExists': - # this will never be executed anymore - module.exit_json(changed=False) - module.fail_json_aws(e, msg="Failed to start execution.") - - module.exit_json(changed=True, **camel_dict_to_snake_dict(res_execution)) - - -def stop_execution(module, sfn_client): - - cause = module.params.get('cause') - error = module.params.get('error') - execution_arn = module.params.get('execution_arn') - - try: - # describe_execution is eventually consistent - execution_status = sfn_client.describe_execution(executionArn=execution_arn)['status'] - if execution_status != 'RUNNING': - check_mode(module, msg='State machine execution is not running.', changed=False) - module.exit_json(changed=False) - - check_mode(module, msg='State machine execution would be stopped.', changed=True) - res = sfn_client.stop_execution( - executionArn=execution_arn, - cause=cause, - error=error - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to stop execution.") - - module.exit_json(changed=True, **camel_dict_to_snake_dict(res)) - - -def check_mode(module, msg='', changed=False): - if module.check_mode: - module.exit_json(changed=changed, output=msg) - - -def main(): - module_args = dict( - action=dict(choices=['start', 'stop'], default='start'), - name=dict(type='str'), - execution_input=dict(type='json', default={}), - state_machine_arn=dict(type='str'), - cause=dict(type='str', default=''), - error=dict(type='str', default=''), - execution_arn=dict(type='str') - ) - module = AnsibleAWSModule( - argument_spec=module_args, - required_if=[('action', 'start', ['name', 'state_machine_arn']), - ('action', 'stop', ['execution_arn']), - ], - supports_check_mode=True - ) - - sfn_client = module.client('stepfunctions') - - action = module.params.get('action') - if action == "start": - start_execution(module, sfn_client) - else: - stop_execution(module, sfn_client) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_condition.py b/lib/ansible/modules/cloud/amazon/aws_waf_condition.py deleted file mode 100644 index 79f891af8d..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_waf_condition.py +++ /dev/null @@ -1,736 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Will Thames -# Copyright (c) 2015 Mike Mochan -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_waf_condition -short_description: Create and delete WAF Conditions -description: - - Read the AWS documentation for WAF - U(https://aws.amazon.com/documentation/waf/) -version_added: "2.5" - -author: - - Will Thames (@willthames) - - Mike Mochan (@mmochan) -extends_documentation_fragment: - - aws - - ec2 -options: - name: - description: Name of the Web Application Firewall condition to manage. - required: true - type: str - type: - description: The type of matching to perform. - choices: - - byte - - geo - - ip - - regex - - size - - sql - - xss - type: str - required: true - filters: - description: - - A list of the filters against which to match. - - For I(type=byte), valid keys are I(field_to_match), I(position), I(header), I(transformation) and I(target_string). - - For I(type=geo), the only valid key is I(country). - - For I(type=ip), the only valid key is I(ip_address). - - For I(type=regex), valid keys are I(field_to_match), I(transformation) and I(regex_pattern). - - For I(type=size), valid keys are I(field_to_match), I(transformation), I(comparison) and I(size). - - For I(type=sql), valid keys are I(field_to_match) and I(transformation). - - For I(type=xss), valid keys are I(field_to_match) and I(transformation). - - Required when I(state=present). - type: list - elements: dict - suboptions: - field_to_match: - description: - - The field upon which to perform the match. - - Valid when I(type=byte), I(type=regex), I(type=sql) or I(type=xss). - type: str - choices: ['uri', 'query_string', 'header', 'method', 'body'] - position: - description: - - Where in the field the match needs to occur. - - Only valid when I(type=byte). - type: str - choices: ['exactly', 'starts_with', 'ends_with', 'contains', 'contains_word'] - header: - description: - - Which specific header should be matched. - - Required when I(field_to_match=header). - - Valid when I(type=byte). - type: str - transformation: - description: - - A transform to apply on the field prior to performing the match. - - Valid when I(type=byte), I(type=regex), I(type=sql) or I(type=xss). - type: str - choices: ['none', 'compress_white_space', 'html_entity_decode', 'lowercase', 'cmd_line', 'url_decode'] - country: - description: - - Value of geo constraint (typically a two letter country code). - - The only valid key when I(type=geo). - type: str - ip_address: - description: - - An IP Address or CIDR to match. - - The only valid key when I(type=ip). - type: str - regex_pattern: - description: - - A dict describing the regular expressions used to perform the match. - - Only valid when I(type=regex). - type: dict - suboptions: - name: - description: A name to describe the set of patterns. - type: str - regex_strings: - description: A list of regular expressions to match. - type: list - elements: str - comparison: - description: - - What type of comparison to perform. - - Only valid key when I(type=size). - type: str - choices: ['EQ', 'NE', 'LE', 'LT', 'GE', 'GT'] - size: - description: - - The size of the field (in bytes). - - Only valid key when I(type=size). - type: int - target_string: - description: - - The string to search for. - - May be up to 50 bytes. - - Valid when I(type=byte). - type: str - purge_filters: - description: - - Whether to remove existing filters from a condition if not passed in I(filters). - default: false - type: bool - waf_regional: - description: Whether to use waf-regional module. - default: false - required: no - type: bool - version_added: 2.9 - state: - description: Whether the condition should be C(present) or C(absent). - choices: - - present - - absent - default: present - type: str - -''' - -EXAMPLES = ''' - - name: create WAF byte condition - aws_waf_condition: - name: my_byte_condition - filters: - - field_to_match: header - position: STARTS_WITH - target_string: Hello - header: Content-type - type: byte - - - name: create WAF geo condition - aws_waf_condition: - name: my_geo_condition - filters: - - country: US - - country: AU - - country: AT - type: geo - - - name: create IP address condition - aws_waf_condition: - name: "{{ resource_prefix }}_ip_condition" - filters: - - ip_address: "10.0.0.0/8" - - ip_address: "192.168.0.0/24" - type: ip - - - name: create WAF regex condition - aws_waf_condition: - name: my_regex_condition - filters: - - field_to_match: query_string - regex_pattern: - name: greetings - regex_strings: - - '[hH]ello' - - '^Hi there' - - '.*Good Day to You' - type: regex - - - name: create WAF size condition - aws_waf_condition: - name: my_size_condition - filters: - - field_to_match: query_string - size: 300 - comparison: GT - type: size - - - name: create WAF sql injection condition - aws_waf_condition: - name: my_sql_condition - filters: - - field_to_match: query_string - transformation: url_decode - type: sql - - - name: create WAF xss condition - aws_waf_condition: - name: my_xss_condition - filters: - - field_to_match: query_string - transformation: url_decode - type: xss - -''' - -RETURN = ''' -condition: - description: Condition returned by operation. - returned: always - type: complex - contains: - condition_id: - description: Type-agnostic ID for the condition. - returned: when state is present - type: str - sample: dd74b1ff-8c06-4a4f-897a-6b23605de413 - byte_match_set_id: - description: ID for byte match set. - returned: always - type: str - sample: c4882c96-837b-44a2-a762-4ea87dbf812b - byte_match_tuples: - description: List of byte match tuples. - returned: always - type: complex - contains: - field_to_match: - description: Field to match. - returned: always - type: complex - contains: - data: - description: Which specific header (if type is header). - type: str - sample: content-type - type: - description: Type of field - type: str - sample: HEADER - positional_constraint: - description: Position in the field to match. - type: str - sample: STARTS_WITH - target_string: - description: String to look for. - type: str - sample: Hello - text_transformation: - description: Transformation to apply to the field before matching. - type: str - sample: NONE - geo_match_constraints: - description: List of geographical constraints. - returned: when type is geo and state is present - type: complex - contains: - type: - description: Type of geo constraint. - type: str - sample: Country - value: - description: Value of geo constraint (typically a country code). - type: str - sample: AT - geo_match_set_id: - description: ID of the geo match set. - returned: when type is geo and state is present - type: str - sample: dd74b1ff-8c06-4a4f-897a-6b23605de413 - ip_set_descriptors: - description: list of IP address filters - returned: when type is ip and state is present - type: complex - contains: - type: - description: Type of IP address (IPV4 or IPV6). - returned: always - type: str - sample: IPV4 - value: - description: IP address. - returned: always - type: str - sample: 10.0.0.0/8 - ip_set_id: - description: ID of condition. - returned: when type is ip and state is present - type: str - sample: 78ad334a-3535-4036-85e6-8e11e745217b - name: - description: Name of condition. - returned: when state is present - type: str - sample: my_waf_condition - regex_match_set_id: - description: ID of the regex match set. - returned: when type is regex and state is present - type: str - sample: 5ea3f6a8-3cd3-488b-b637-17b79ce7089c - regex_match_tuples: - description: List of regex matches. - returned: when type is regex and state is present - type: complex - contains: - field_to_match: - description: Field on which the regex match is applied. - type: complex - contains: - type: - description: The field name. - returned: when type is regex and state is present - type: str - sample: QUERY_STRING - regex_pattern_set_id: - description: ID of the regex pattern. - type: str - sample: 6fdf7f2d-9091-445c-aef2-98f3c051ac9e - text_transformation: - description: transformation applied to the text before matching - type: str - sample: NONE - size_constraint_set_id: - description: ID of the size constraint set. - returned: when type is size and state is present - type: str - sample: de84b4b3-578b-447e-a9a0-0db35c995656 - size_constraints: - description: List of size constraints to apply. - returned: when type is size and state is present - type: complex - contains: - comparison_operator: - description: Comparison operator to apply. - type: str - sample: GT - field_to_match: - description: Field on which the size constraint is applied. - type: complex - contains: - type: - description: Field name. - type: str - sample: QUERY_STRING - size: - description: Size to compare against the field. - type: int - sample: 300 - text_transformation: - description: Transformation applied to the text before matching. - type: str - sample: NONE - sql_injection_match_set_id: - description: ID of the SQL injection match set. - returned: when type is sql and state is present - type: str - sample: de84b4b3-578b-447e-a9a0-0db35c995656 - sql_injection_match_tuples: - description: List of SQL injection match sets. - returned: when type is sql and state is present - type: complex - contains: - field_to_match: - description: Field on which the SQL injection match is applied. - type: complex - contains: - type: - description: Field name. - type: str - sample: QUERY_STRING - text_transformation: - description: Transformation applied to the text before matching. - type: str - sample: URL_DECODE - xss_match_set_id: - description: ID of the XSS match set. - returned: when type is xss and state is present - type: str - sample: de84b4b3-578b-447e-a9a0-0db35c995656 - xss_match_tuples: - description: List of XSS match sets. - returned: when type is xss and state is present - type: complex - contains: - field_to_match: - description: Field on which the XSS match is applied. - type: complex - contains: - type: - description: Field name - type: str - sample: QUERY_STRING - text_transformation: - description: transformation applied to the text before matching. - type: str - sample: URL_DECODE -''' - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, compare_policies -from ansible.module_utils.aws.waf import run_func_with_change_token_backoff, MATCH_LOOKUP -from ansible.module_utils.aws.waf import get_rule_with_backoff, list_rules_with_backoff, list_regional_rules_with_backoff - - -class Condition(object): - - def __init__(self, client, module): - self.client = client - self.module = module - self.type = module.params['type'] - self.method_suffix = MATCH_LOOKUP[self.type]['method'] - self.conditionset = MATCH_LOOKUP[self.type]['conditionset'] - self.conditionsets = MATCH_LOOKUP[self.type]['conditionset'] + 's' - self.conditionsetid = MATCH_LOOKUP[self.type]['conditionset'] + 'Id' - self.conditiontuple = MATCH_LOOKUP[self.type]['conditiontuple'] - self.conditiontuples = MATCH_LOOKUP[self.type]['conditiontuple'] + 's' - self.conditiontype = MATCH_LOOKUP[self.type]['type'] - - def format_for_update(self, condition_set_id): - # Prep kwargs - kwargs = dict() - kwargs['Updates'] = list() - - for filtr in self.module.params.get('filters'): - # Only for ip_set - if self.type == 'ip': - # there might be a better way of detecting an IPv6 address - if ':' in filtr.get('ip_address'): - ip_type = 'IPV6' - else: - ip_type = 'IPV4' - condition_insert = {'Type': ip_type, 'Value': filtr.get('ip_address')} - - # Specific for geo_match_set - if self.type == 'geo': - condition_insert = dict(Type='Country', Value=filtr.get('country')) - - # Common For everything but ip_set and geo_match_set - if self.type not in ('ip', 'geo'): - - condition_insert = dict(FieldToMatch=dict(Type=filtr.get('field_to_match').upper()), - TextTransformation=filtr.get('transformation', 'none').upper()) - - if filtr.get('field_to_match').upper() == "HEADER": - if filtr.get('header'): - condition_insert['FieldToMatch']['Data'] = filtr.get('header').lower() - else: - self.module.fail_json(msg=str("DATA required when HEADER requested")) - - # Specific for byte_match_set - if self.type == 'byte': - condition_insert['TargetString'] = filtr.get('target_string') - condition_insert['PositionalConstraint'] = filtr.get('position') - - # Specific for size_constraint_set - if self.type == 'size': - condition_insert['ComparisonOperator'] = filtr.get('comparison') - condition_insert['Size'] = filtr.get('size') - - # Specific for regex_match_set - if self.type == 'regex': - condition_insert['RegexPatternSetId'] = self.ensure_regex_pattern_present(filtr.get('regex_pattern'))['RegexPatternSetId'] - - kwargs['Updates'].append({'Action': 'INSERT', self.conditiontuple: condition_insert}) - - kwargs[self.conditionsetid] = condition_set_id - return kwargs - - def format_for_deletion(self, condition): - return {'Updates': [{'Action': 'DELETE', self.conditiontuple: current_condition_tuple} - for current_condition_tuple in condition[self.conditiontuples]], - self.conditionsetid: condition[self.conditionsetid]} - - @AWSRetry.exponential_backoff() - def list_regex_patterns_with_backoff(self, **params): - return self.client.list_regex_pattern_sets(**params) - - @AWSRetry.exponential_backoff() - def get_regex_pattern_set_with_backoff(self, regex_pattern_set_id): - return self.client.get_regex_pattern_set(RegexPatternSetId=regex_pattern_set_id) - - def list_regex_patterns(self): - # at time of writing(2017-11-20) no regex pattern paginator exists - regex_patterns = [] - params = {} - while True: - try: - response = self.list_regex_patterns_with_backoff(**params) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not list regex patterns') - regex_patterns.extend(response['RegexPatternSets']) - if 'NextMarker' in response: - params['NextMarker'] = response['NextMarker'] - else: - break - return regex_patterns - - def get_regex_pattern_by_name(self, name): - existing_regex_patterns = self.list_regex_patterns() - regex_lookup = dict((item['Name'], item['RegexPatternSetId']) for item in existing_regex_patterns) - if name in regex_lookup: - return self.get_regex_pattern_set_with_backoff(regex_lookup[name])['RegexPatternSet'] - else: - return None - - def ensure_regex_pattern_present(self, regex_pattern): - name = regex_pattern['name'] - - pattern_set = self.get_regex_pattern_by_name(name) - if not pattern_set: - pattern_set = run_func_with_change_token_backoff(self.client, self.module, {'Name': name}, - self.client.create_regex_pattern_set)['RegexPatternSet'] - missing = set(regex_pattern['regex_strings']) - set(pattern_set['RegexPatternStrings']) - extra = set(pattern_set['RegexPatternStrings']) - set(regex_pattern['regex_strings']) - if not missing and not extra: - return pattern_set - updates = [{'Action': 'INSERT', 'RegexPatternString': pattern} for pattern in missing] - updates.extend([{'Action': 'DELETE', 'RegexPatternString': pattern} for pattern in extra]) - run_func_with_change_token_backoff(self.client, self.module, - {'RegexPatternSetId': pattern_set['RegexPatternSetId'], 'Updates': updates}, - self.client.update_regex_pattern_set, wait=True) - return self.get_regex_pattern_set_with_backoff(pattern_set['RegexPatternSetId'])['RegexPatternSet'] - - def delete_unused_regex_pattern(self, regex_pattern_set_id): - try: - regex_pattern_set = self.client.get_regex_pattern_set(RegexPatternSetId=regex_pattern_set_id)['RegexPatternSet'] - updates = list() - for regex_pattern_string in regex_pattern_set['RegexPatternStrings']: - updates.append({'Action': 'DELETE', 'RegexPatternString': regex_pattern_string}) - run_func_with_change_token_backoff(self.client, self.module, - {'RegexPatternSetId': regex_pattern_set_id, 'Updates': updates}, - self.client.update_regex_pattern_set) - - run_func_with_change_token_backoff(self.client, self.module, - {'RegexPatternSetId': regex_pattern_set_id}, - self.client.delete_regex_pattern_set, wait=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - if e.response['Error']['Code'] == 'WAFNonexistentItemException': - return - self.module.fail_json_aws(e, msg='Could not delete regex pattern') - - def get_condition_by_name(self, name): - all_conditions = [d for d in self.list_conditions() if d['Name'] == name] - if all_conditions: - return all_conditions[0][self.conditionsetid] - - @AWSRetry.exponential_backoff() - def get_condition_by_id_with_backoff(self, condition_set_id): - params = dict() - params[self.conditionsetid] = condition_set_id - func = getattr(self.client, 'get_' + self.method_suffix) - return func(**params)[self.conditionset] - - def get_condition_by_id(self, condition_set_id): - try: - return self.get_condition_by_id_with_backoff(condition_set_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not get condition') - - def list_conditions(self): - method = 'list_' + self.method_suffix + 's' - try: - paginator = self.client.get_paginator(method) - func = paginator.paginate().build_full_result - except botocore.exceptions.OperationNotPageableError: - # list_geo_match_sets and list_regex_match_sets do not have a paginator - func = getattr(self.client, method) - try: - return func()[self.conditionsets] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not list %s conditions' % self.type) - - def tidy_up_regex_patterns(self, regex_match_set): - all_regex_match_sets = self.list_conditions() - all_match_set_patterns = list() - for rms in all_regex_match_sets: - all_match_set_patterns.extend(conditiontuple['RegexPatternSetId'] - for conditiontuple in self.get_condition_by_id(rms[self.conditionsetid])[self.conditiontuples]) - for filtr in regex_match_set[self.conditiontuples]: - if filtr['RegexPatternSetId'] not in all_match_set_patterns: - self.delete_unused_regex_pattern(filtr['RegexPatternSetId']) - - def find_condition_in_rules(self, condition_set_id): - rules_in_use = [] - try: - if self.client.__class__.__name__ == 'WAF': - all_rules = list_rules_with_backoff(self.client) - elif self.client.__class__.__name__ == 'WAFRegional': - all_rules = list_regional_rules_with_backoff(self.client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not list rules') - for rule in all_rules: - try: - rule_details = get_rule_with_backoff(self.client, rule['RuleId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not get rule details') - if condition_set_id in [predicate['DataId'] for predicate in rule_details['Predicates']]: - rules_in_use.append(rule_details['Name']) - return rules_in_use - - def find_and_delete_condition(self, condition_set_id): - current_condition = self.get_condition_by_id(condition_set_id) - in_use_rules = self.find_condition_in_rules(condition_set_id) - if in_use_rules: - rulenames = ', '.join(in_use_rules) - self.module.fail_json(msg="Condition %s is in use by %s" % (current_condition['Name'], rulenames)) - if current_condition[self.conditiontuples]: - # Filters are deleted using update with the DELETE action - func = getattr(self.client, 'update_' + self.method_suffix) - params = self.format_for_deletion(current_condition) - try: - # We do not need to wait for the conditiontuple delete because we wait later for the delete_* call - run_func_with_change_token_backoff(self.client, self.module, params, func) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not delete filters from condition') - func = getattr(self.client, 'delete_' + self.method_suffix) - params = dict() - params[self.conditionsetid] = condition_set_id - try: - run_func_with_change_token_backoff(self.client, self.module, params, func, wait=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not delete condition') - # tidy up regex patterns - if self.type == 'regex': - self.tidy_up_regex_patterns(current_condition) - return True, {} - - def find_missing(self, update, current_condition): - missing = [] - for desired in update['Updates']: - found = False - desired_condition = desired[self.conditiontuple] - current_conditions = current_condition[self.conditiontuples] - for condition in current_conditions: - if not compare_policies(condition, desired_condition): - found = True - if not found: - missing.append(desired) - return missing - - def find_and_update_condition(self, condition_set_id): - current_condition = self.get_condition_by_id(condition_set_id) - update = self.format_for_update(condition_set_id) - missing = self.find_missing(update, current_condition) - if self.module.params.get('purge_filters'): - extra = [{'Action': 'DELETE', self.conditiontuple: current_tuple} - for current_tuple in current_condition[self.conditiontuples] - if current_tuple not in [desired[self.conditiontuple] for desired in update['Updates']]] - else: - extra = [] - changed = bool(missing or extra) - if changed: - update['Updates'] = missing + extra - func = getattr(self.client, 'update_' + self.method_suffix) - try: - result = run_func_with_change_token_backoff(self.client, self.module, update, func, wait=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not update condition') - return changed, self.get_condition_by_id(condition_set_id) - - def ensure_condition_present(self): - name = self.module.params['name'] - condition_set_id = self.get_condition_by_name(name) - if condition_set_id: - return self.find_and_update_condition(condition_set_id) - else: - params = dict() - params['Name'] = name - func = getattr(self.client, 'create_' + self.method_suffix) - try: - condition = run_func_with_change_token_backoff(self.client, self.module, params, func) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg='Could not create condition') - return self.find_and_update_condition(condition[self.conditionset][self.conditionsetid]) - - def ensure_condition_absent(self): - condition_set_id = self.get_condition_by_name(self.module.params['name']) - if condition_set_id: - return self.find_and_delete_condition(condition_set_id) - return False, {} - - -def main(): - filters_subspec = dict( - country=dict(), - field_to_match=dict(choices=['uri', 'query_string', 'header', 'method', 'body']), - header=dict(), - transformation=dict(choices=['none', 'compress_white_space', - 'html_entity_decode', 'lowercase', - 'cmd_line', 'url_decode']), - position=dict(choices=['exactly', 'starts_with', 'ends_with', - 'contains', 'contains_word']), - comparison=dict(choices=['EQ', 'NE', 'LE', 'LT', 'GE', 'GT']), - target_string=dict(), # Bytes - size=dict(type='int'), - ip_address=dict(), - regex_pattern=dict(), - ) - argument_spec = dict( - name=dict(required=True), - type=dict(required=True, choices=['byte', 'geo', 'ip', 'regex', 'size', 'sql', 'xss']), - filters=dict(type='list'), - purge_filters=dict(type='bool', default=False), - waf_regional=dict(type='bool', default=False), - state=dict(default='present', choices=['present', 'absent']), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['filters']]]) - state = module.params.get('state') - - resource = 'waf' if not module.params['waf_regional'] else 'waf-regional' - client = module.client(resource) - - condition = Condition(client, module) - - if state == 'present': - (changed, results) = condition.ensure_condition_present() - # return a condition agnostic ID for use by aws_waf_rule - results['ConditionId'] = results[condition.conditionsetid] - else: - (changed, results) = condition.ensure_condition_absent() - - module.exit_json(changed=changed, condition=camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_info.py b/lib/ansible/modules/cloud/amazon/aws_waf_info.py deleted file mode 100644 index 428c077e01..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_waf_info.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_waf_info -short_description: Retrieve information for WAF ACLs, Rule , Conditions and Filters. -description: - - Retrieve information for WAF ACLs, Rule , Conditions and Filters. - - This module was called C(aws_waf_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -options: - name: - description: - - The name of a Web Application Firewall. - type: str - waf_regional: - description: Whether to use the waf-regional module. - default: false - required: no - type: bool - version_added: "2.9" - -author: - - Mike Mochan (@mmochan) - - Will Thames (@willthames) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: obtain all WAF information - aws_waf_info: - -- name: obtain all information for a single WAF - aws_waf_info: - name: test_waf - -- name: obtain all information for a single WAF Regional - aws_waf_info: - name: test_waf - waf_regional: true -''' - -RETURN = ''' -wafs: - description: The WAFs that match the passed arguments. - returned: success - type: complex - contains: - name: - description: A friendly name or description of the WebACL. - returned: always - type: str - sample: test_waf - default_action: - description: The action to perform if none of the Rules contained in the WebACL match. - returned: always - type: int - sample: BLOCK - metric_name: - description: A friendly name or description for the metrics for this WebACL. - returned: always - type: str - sample: test_waf_metric - rules: - description: An array that contains the action for each Rule in a WebACL , the priority of the Rule. - returned: always - type: complex - contains: - action: - description: The action to perform if the Rule matches. - returned: always - type: str - sample: BLOCK - metric_name: - description: A friendly name or description for the metrics for this Rule. - returned: always - type: str - sample: ipblockrule - name: - description: A friendly name or description of the Rule. - returned: always - type: str - sample: ip_block_rule - predicates: - description: The Predicates list contains a Predicate for each - ByteMatchSet, IPSet, SizeConstraintSet, SqlInjectionMatchSet or XssMatchSet - object in a Rule. - returned: always - type: list - sample: - [ - { - "byte_match_set_id": "47b822b5-abcd-1234-faaf-1234567890", - "byte_match_tuples": [ - { - "field_to_match": { - "type": "QUERY_STRING" - }, - "positional_constraint": "STARTS_WITH", - "target_string": "bobbins", - "text_transformation": "NONE" - } - ], - "name": "bobbins", - "negated": false, - "type": "ByteMatch" - } - ] -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waf import list_web_acls, get_web_acl - - -def main(): - argument_spec = dict( - name=dict(required=False), - waf_regional=dict(type='bool', default=False) - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'aws_waf_facts': - module.deprecate("The 'aws_waf_facts' module has been renamed to 'aws_waf_info'", version='2.13') - - resource = 'waf' if not module.params['waf_regional'] else 'waf-regional' - client = module.client(resource) - web_acls = list_web_acls(client, module) - name = module.params['name'] - if name: - web_acls = [web_acl for web_acl in web_acls if - web_acl['Name'] == name] - if not web_acls: - module.fail_json(msg="WAF named %s not found" % name) - module.exit_json(wafs=[get_web_acl(client, module, web_acl['WebACLId']) - for web_acl in web_acls]) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_rule.py b/lib/ansible/modules/cloud/amazon/aws_waf_rule.py deleted file mode 100644 index 358c42c696..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_waf_rule.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Will Thames -# Copyright (c) 2015 Mike Mochan -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_waf_rule -short_description: Create and delete WAF Rules -description: - - Read the AWS documentation for WAF - U(https://aws.amazon.com/documentation/waf/). -version_added: "2.5" - -author: - - Mike Mochan (@mmochan) - - Will Thames (@willthames) -extends_documentation_fragment: - - aws - - ec2 -options: - name: - description: Name of the Web Application Firewall rule. - required: yes - type: str - metric_name: - description: - - A friendly name or description for the metrics for the rule. - - The name can contain only alphanumeric characters (A-Z, a-z, 0-9); the name can't contain whitespace. - - You can't change I(metric_name) after you create the rule. - - Defaults to the same as I(name) with disallowed characters removed. - type: str - state: - description: Whether the rule should be present or absent. - choices: - - present - - absent - default: present - type: str - conditions: - description: > - List of conditions used in the rule. M(aws_waf_condition) can be used to - create new conditions. - type: list - elements: dict - suboptions: - type: - required: true - type: str - choices: ['byte','geo','ip','size','sql','xss'] - description: The type of rule to match. - negated: - required: true - type: bool - description: Whether the condition should be negated. - condition: - required: true - type: str - description: The name of the condition. The condition must already exist. - purge_conditions: - description: - - Whether or not to remove conditions that are not passed when updating `conditions`. - default: false - type: bool - waf_regional: - description: Whether to use waf-regional module. - default: false - required: false - type: bool - version_added: "2.9" -''' - -EXAMPLES = ''' - - - name: create WAF rule - aws_waf_rule: - name: my_waf_rule - conditions: - - name: my_regex_condition - type: regex - negated: no - - name: my_geo_condition - type: geo - negated: no - - name: my_byte_condition - type: byte - negated: yes - - - name: remove WAF rule - aws_waf_rule: - name: "my_waf_rule" - state: absent - -''' - -RETURN = ''' -rule: - description: WAF rule contents - returned: always - type: complex - contains: - metric_name: - description: Metric name for the rule. - returned: always - type: str - sample: ansibletest1234rule - name: - description: Friendly name for the rule. - returned: always - type: str - sample: ansible-test-1234_rule - predicates: - description: List of conditions used in the rule. - returned: always - type: complex - contains: - data_id: - description: ID of the condition. - returned: always - type: str - sample: 8251acdb-526c-42a8-92bc-d3d13e584166 - negated: - description: Whether the sense of the condition is negated. - returned: always - type: bool - sample: false - type: - description: type of the condition. - returned: always - type: str - sample: ByteMatch - rule_id: - description: ID of the WAF rule. - returned: always - type: str - sample: 15de0cbc-9204-4e1f-90e6-69b2f415c261 -''' - -import re - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils.aws.waf import run_func_with_change_token_backoff, list_rules_with_backoff, list_regional_rules_with_backoff, MATCH_LOOKUP -from ansible.module_utils.aws.waf import get_web_acl_with_backoff, list_web_acls_with_backoff, list_regional_web_acls_with_backoff - - -def get_rule_by_name(client, module, name): - rules = [d['RuleId'] for d in list_rules(client, module) if d['Name'] == name] - if rules: - return rules[0] - - -def get_rule(client, module, rule_id): - try: - return client.get_rule(RuleId=rule_id)['Rule'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not get WAF rule') - - -def list_rules(client, module): - if client.__class__.__name__ == 'WAF': - try: - return list_rules_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list WAF rules') - elif client.__class__.__name__ == 'WAFRegional': - try: - return list_regional_rules_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list WAF Regional rules') - - -def list_regional_rules(client, module): - try: - return list_regional_rules_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list WAF rules') - - -def find_and_update_rule(client, module, rule_id): - rule = get_rule(client, module, rule_id) - rule_id = rule['RuleId'] - - existing_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP) - desired_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP) - all_conditions = dict() - - for condition_type in MATCH_LOOKUP: - method = 'list_' + MATCH_LOOKUP[condition_type]['method'] + 's' - all_conditions[condition_type] = dict() - try: - paginator = client.get_paginator(method) - func = paginator.paginate().build_full_result - except (KeyError, botocore.exceptions.OperationNotPageableError): - # list_geo_match_sets and list_regex_match_sets do not have a paginator - # and throw different exceptions - func = getattr(client, method) - try: - pred_results = func()[MATCH_LOOKUP[condition_type]['conditionset'] + 's'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list %s conditions' % condition_type) - for pred in pred_results: - pred['DataId'] = pred[MATCH_LOOKUP[condition_type]['conditionset'] + 'Id'] - all_conditions[condition_type][pred['Name']] = camel_dict_to_snake_dict(pred) - all_conditions[condition_type][pred['DataId']] = camel_dict_to_snake_dict(pred) - - for condition in module.params['conditions']: - desired_conditions[condition['type']][condition['name']] = condition - - reverse_condition_types = dict((v['type'], k) for (k, v) in MATCH_LOOKUP.items()) - for condition in rule['Predicates']: - existing_conditions[reverse_condition_types[condition['Type']]][condition['DataId']] = camel_dict_to_snake_dict(condition) - - insertions = list() - deletions = list() - - for condition_type in desired_conditions: - for (condition_name, condition) in desired_conditions[condition_type].items(): - if condition_name not in all_conditions[condition_type]: - module.fail_json(msg="Condition %s of type %s does not exist" % (condition_name, condition_type)) - condition['data_id'] = all_conditions[condition_type][condition_name]['data_id'] - if condition['data_id'] not in existing_conditions[condition_type]: - insertions.append(format_for_insertion(condition)) - - if module.params['purge_conditions']: - for condition_type in existing_conditions: - deletions.extend([format_for_deletion(condition) for condition in existing_conditions[condition_type].values() - if not all_conditions[condition_type][condition['data_id']]['name'] in desired_conditions[condition_type]]) - - changed = bool(insertions or deletions) - update = { - 'RuleId': rule_id, - 'Updates': insertions + deletions - } - if changed: - try: - run_func_with_change_token_backoff(client, module, update, client.update_rule, wait=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not update rule conditions') - - return changed, get_rule(client, module, rule_id) - - -def format_for_insertion(condition): - return dict(Action='INSERT', - Predicate=dict(Negated=condition['negated'], - Type=MATCH_LOOKUP[condition['type']]['type'], - DataId=condition['data_id'])) - - -def format_for_deletion(condition): - return dict(Action='DELETE', - Predicate=dict(Negated=condition['negated'], - Type=condition['type'], - DataId=condition['data_id'])) - - -def remove_rule_conditions(client, module, rule_id): - conditions = get_rule(client, module, rule_id)['Predicates'] - updates = [format_for_deletion(camel_dict_to_snake_dict(condition)) for condition in conditions] - try: - run_func_with_change_token_backoff(client, module, {'RuleId': rule_id, 'Updates': updates}, client.update_rule) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not remove rule conditions') - - -def ensure_rule_present(client, module): - name = module.params['name'] - rule_id = get_rule_by_name(client, module, name) - params = dict() - if rule_id: - return find_and_update_rule(client, module, rule_id) - else: - params['Name'] = module.params['name'] - metric_name = module.params['metric_name'] - if not metric_name: - metric_name = re.sub(r'[^a-zA-Z0-9]', '', module.params['name']) - params['MetricName'] = metric_name - try: - new_rule = run_func_with_change_token_backoff(client, module, params, client.create_rule)['Rule'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not create rule') - return find_and_update_rule(client, module, new_rule['RuleId']) - - -def find_rule_in_web_acls(client, module, rule_id): - web_acls_in_use = [] - try: - if client.__class__.__name__ == 'WAF': - all_web_acls = list_web_acls_with_backoff(client) - elif client.__class__.__name__ == 'WAFRegional': - all_web_acls = list_regional_web_acls_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list Web ACLs') - for web_acl in all_web_acls: - try: - web_acl_details = get_web_acl_with_backoff(client, web_acl['WebACLId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not get Web ACL details') - if rule_id in [rule['RuleId'] for rule in web_acl_details['Rules']]: - web_acls_in_use.append(web_acl_details['Name']) - return web_acls_in_use - - -def ensure_rule_absent(client, module): - rule_id = get_rule_by_name(client, module, module.params['name']) - in_use_web_acls = find_rule_in_web_acls(client, module, rule_id) - if in_use_web_acls: - web_acl_names = ', '.join(in_use_web_acls) - module.fail_json(msg="Rule %s is in use by Web ACL(s) %s" % - (module.params['name'], web_acl_names)) - if rule_id: - remove_rule_conditions(client, module, rule_id) - try: - return True, run_func_with_change_token_backoff(client, module, {'RuleId': rule_id}, client.delete_rule, wait=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not delete rule') - return False, {} - - -def main(): - argument_spec = dict( - name=dict(required=True), - metric_name=dict(), - state=dict(default='present', choices=['present', 'absent']), - conditions=dict(type='list'), - purge_conditions=dict(type='bool', default=False), - waf_regional=dict(type='bool', default=False), - ) - module = AnsibleAWSModule(argument_spec=argument_spec) - state = module.params.get('state') - - resource = 'waf' if not module.params['waf_regional'] else 'waf-regional' - client = module.client(resource) - if state == 'present': - (changed, results) = ensure_rule_present(client, module) - else: - (changed, results) = ensure_rule_absent(client, module) - - module.exit_json(changed=changed, rule=camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py b/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py deleted file mode 100644 index d6cd4dea26..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: aws_waf_web_acl -short_description: Create and delete WAF Web ACLs. -description: - - Read the AWS documentation for WAF - U(https://aws.amazon.com/documentation/waf/). -version_added: "2.5" - -author: - - Mike Mochan (@mmochan) - - Will Thames (@willthames) -extends_documentation_fragment: - - aws - - ec2 -options: - name: - description: Name of the Web Application Firewall ACL to manage. - required: yes - type: str - default_action: - description: The action that you want AWS WAF to take when a request doesn't - match the criteria specified in any of the Rule objects that are associated with the WebACL. - choices: - - block - - allow - - count - type: str - state: - description: Whether the Web ACL should be present or absent. - choices: - - present - - absent - default: present - type: str - metric_name: - description: - - A friendly name or description for the metrics for this WebACL. - - The name can contain only alphanumeric characters (A-Z, a-z, 0-9); the name can't contain whitespace. - - You can't change I(metric_name) after you create the WebACL. - - Metric name will default to I(name) with disallowed characters stripped out. - type: str - rules: - description: - - A list of rules that the Web ACL will enforce. - type: list - elements: dict - suboptions: - name: - description: Name of the rule. - type: str - required: true - action: - description: The action to perform. - type: str - required: true - priority: - description: The priority of the action. Priorities must be unique. Lower numbered priorities are evaluated first. - type: int - required: true - type: - description: The type of rule. - choices: - - rate_based - - regular - type: str - purge_rules: - description: - - Whether to remove rules that aren't passed with I(rules). - default: False - type: bool - waf_regional: - description: Whether to use waf-regional module. - default: false - required: no - type: bool - version_added: "2.9" -''' - -EXAMPLES = ''' - - name: create web ACL - aws_waf_web_acl: - name: my_web_acl - rules: - - name: my_rule - priority: 1 - action: block - default_action: block - purge_rules: yes - state: present - - - name: delete the web acl - aws_waf_web_acl: - name: my_web_acl - state: absent -''' - -RETURN = ''' -web_acl: - description: contents of the Web ACL. - returned: always - type: complex - contains: - default_action: - description: Default action taken by the Web ACL if no rules match. - returned: always - type: dict - sample: - type: BLOCK - metric_name: - description: Metric name used as an identifier. - returned: always - type: str - sample: mywebacl - name: - description: Friendly name of the Web ACL. - returned: always - type: str - sample: my web acl - rules: - description: List of rules. - returned: always - type: complex - contains: - action: - description: Action taken by the WAF when the rule matches. - returned: always - type: complex - sample: - type: ALLOW - priority: - description: priority number of the rule (lower numbers are run first). - returned: always - type: int - sample: 2 - rule_id: - description: Rule ID. - returned: always - type: str - sample: a6fc7ab5-287b-479f-8004-7fd0399daf75 - type: - description: Type of rule (either REGULAR or RATE_BASED). - returned: always - type: str - sample: REGULAR - web_acl_id: - description: Unique identifier of Web ACL. - returned: always - type: str - sample: 10fff965-4b6b-46e2-9d78-24f6d2e2d21c -''' - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -import re - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils.aws.waf import list_rules_with_backoff, list_web_acls_with_backoff, list_regional_web_acls_with_backoff, \ - run_func_with_change_token_backoff, list_regional_rules_with_backoff - - -def get_web_acl_by_name(client, module, name): - acls = [d['WebACLId'] for d in list_web_acls(client, module) if d['Name'] == name] - if acls: - return acls[0] - else: - return acls - - -def create_rule_lookup(client, module): - if client.__class__.__name__ == 'WAF': - try: - rules = list_rules_with_backoff(client) - return dict((rule['Name'], rule) for rule in rules) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list rules') - elif client.__class__.__name__ == 'WAFRegional': - try: - rules = list_regional_rules_with_backoff(client) - return dict((rule['Name'], rule) for rule in rules) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not list regional rules') - - -def get_web_acl(client, module, web_acl_id): - try: - return client.get_web_acl(WebACLId=web_acl_id)['WebACL'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not get Web ACL with id %s' % web_acl_id) - - -def list_web_acls(client, module,): - if client.__class__.__name__ == 'WAF': - try: - return list_web_acls_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not get Web ACLs') - elif client.__class__.__name__ == 'WAFRegional': - try: - return list_regional_web_acls_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not get Web ACLs') - - -def find_and_update_web_acl(client, module, web_acl_id): - acl = get_web_acl(client, module, web_acl_id) - rule_lookup = create_rule_lookup(client, module) - existing_rules = acl['Rules'] - desired_rules = [{'RuleId': rule_lookup[rule['name']]['RuleId'], - 'Priority': rule['priority'], - 'Action': {'Type': rule['action'].upper()}, - 'Type': rule.get('type', 'regular').upper()} - for rule in module.params['rules']] - missing = [rule for rule in desired_rules if rule not in existing_rules] - extras = [] - if module.params['purge_rules']: - extras = [rule for rule in existing_rules if rule not in desired_rules] - - insertions = [format_for_update(rule, 'INSERT') for rule in missing] - deletions = [format_for_update(rule, 'DELETE') for rule in extras] - changed = bool(insertions + deletions) - - # Purge rules before adding new ones in case a deletion shares the same - # priority as an insertion. - params = { - 'WebACLId': acl['WebACLId'], - 'DefaultAction': acl['DefaultAction'] - } - change_tokens = [] - if deletions: - try: - params['Updates'] = deletions - result = run_func_with_change_token_backoff(client, module, params, client.update_web_acl) - change_tokens.append(result['ChangeToken']) - get_waiter( - client, 'change_token_in_sync', - ).wait( - ChangeToken=result['ChangeToken'] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not update Web ACL') - if insertions: - try: - params['Updates'] = insertions - result = run_func_with_change_token_backoff(client, module, params, client.update_web_acl) - change_tokens.append(result['ChangeToken']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not update Web ACL') - if change_tokens: - for token in change_tokens: - get_waiter( - client, 'change_token_in_sync', - ).wait( - ChangeToken=token - ) - if changed: - acl = get_web_acl(client, module, web_acl_id) - return changed, acl - - -def format_for_update(rule, action): - return dict( - Action=action, - ActivatedRule=dict( - Priority=rule['Priority'], - RuleId=rule['RuleId'], - Action=dict( - Type=rule['Action']['Type'] - ) - ) - ) - - -def remove_rules_from_web_acl(client, module, web_acl_id): - acl = get_web_acl(client, module, web_acl_id) - deletions = [format_for_update(rule, 'DELETE') for rule in acl['Rules']] - try: - params = {'WebACLId': acl['WebACLId'], 'DefaultAction': acl['DefaultAction'], 'Updates': deletions} - run_func_with_change_token_backoff(client, module, params, client.update_web_acl) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not remove rule') - - -def ensure_web_acl_present(client, module): - changed = False - result = None - name = module.params['name'] - web_acl_id = get_web_acl_by_name(client, module, name) - if web_acl_id: - (changed, result) = find_and_update_web_acl(client, module, web_acl_id) - else: - metric_name = module.params['metric_name'] - if not metric_name: - metric_name = re.sub(r'[^A-Za-z0-9]', '', module.params['name']) - default_action = module.params['default_action'].upper() - try: - params = {'Name': name, 'MetricName': metric_name, 'DefaultAction': {'Type': default_action}} - new_web_acl = run_func_with_change_token_backoff(client, module, params, client.create_web_acl) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not create Web ACL') - (changed, result) = find_and_update_web_acl(client, module, new_web_acl['WebACL']['WebACLId']) - return changed, result - - -def ensure_web_acl_absent(client, module): - web_acl_id = get_web_acl_by_name(client, module, module.params['name']) - if web_acl_id: - web_acl = get_web_acl(client, module, web_acl_id) - if web_acl['Rules']: - remove_rules_from_web_acl(client, module, web_acl_id) - try: - run_func_with_change_token_backoff(client, module, {'WebACLId': web_acl_id}, client.delete_web_acl, wait=True) - return True, {} - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Could not delete Web ACL') - return False, {} - - -def main(): - argument_spec = dict( - name=dict(required=True), - default_action=dict(choices=['block', 'allow', 'count']), - metric_name=dict(), - state=dict(default='present', choices=['present', 'absent']), - rules=dict(type='list'), - purge_rules=dict(type='bool', default=False), - waf_regional=dict(type='bool', default=False) - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['default_action', 'rules']]]) - state = module.params.get('state') - - resource = 'waf' if not module.params['waf_regional'] else 'waf-regional' - client = module.client(resource) - if state == 'present': - (changed, results) = ensure_web_acl_present(client, module) - else: - (changed, results) = ensure_web_acl_absent(client, module) - - module.exit_json(changed=changed, web_acl=camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudformation_exports_info.py b/lib/ansible/modules/cloud/amazon/cloudformation_exports_info.py deleted file mode 100644 index a8f79774f0..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudformation_exports_info.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: cloudformation_exports_info -short_description: Read a value from CloudFormation Exports -description: - - Module retrieves a value from CloudFormation Exports -requirements: ['boto3 >= 1.11.15'] -version_added: "2.10" -author: - - "Michael Moyle (@mmoyle)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Get Exports - cloudformation_exports_info: - profile: 'my_aws_profile' - region: 'my_region' - register: cf_exports -- debug: - msg: "{{ cf_exports }}" -''' - -RETURN = ''' -export_items: - description: A dictionary of Exports items names and values. - returned: Always - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry - -try: - from botocore.exceptions import ClientError - from botocore.exceptions import BotoCoreError -except ImportError: - pass # handled by AnsibleAWSModule - - -@AWSRetry.exponential_backoff() -def list_exports(cloudformation_client): - '''Get Exports Names and Values and return in dictionary ''' - list_exports_paginator = cloudformation_client.get_paginator('list_exports') - exports = list_exports_paginator.paginate().build_full_result()['Exports'] - export_items = dict() - - for item in exports: - export_items[item['Name']] = item['Value'] - - return export_items - - -def main(): - argument_spec = dict() - result = dict( - changed=False, - original_message='' - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=False) - cloudformation_client = module.client('cloudformation') - - try: - result['export_items'] = list_exports(cloudformation_client) - - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e) - - result.update() - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudformation_stack_set.py b/lib/ansible/modules/cloud/amazon/cloudformation_stack_set.py deleted file mode 100644 index ba6c2576f4..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudformation_stack_set.py +++ /dev/null @@ -1,724 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2018, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudformation_stack_set -short_description: Manage groups of CloudFormation stacks -description: - - Launches/updates/deletes AWS CloudFormation Stack Sets. -notes: - - To make an individual stack, you want the M(cloudformation) module. -version_added: "2.7" -options: - name: - description: - - Name of the CloudFormation stack set. - required: true - type: str - description: - description: - - A description of what this stack set creates. - type: str - parameters: - description: - - A list of hashes of all the template variables for the stack. The value can be a string or a dict. - - Dict can be used to set additional template parameter attributes like UsePreviousValue (see example). - default: {} - type: dict - state: - description: - - If I(state=present), stack will be created. If I(state=present) and if stack exists and template has changed, it will be updated. - If I(state=absent), stack will be removed. - default: present - choices: [ present, absent ] - type: str - template: - description: - - The local path of the CloudFormation template. - - This must be the full path to the file, relative to the working directory. If using roles this may look - like C(roles/cloudformation/files/cloudformation-example.json). - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) - are specified, the previous template will be reused. - type: path - template_body: - description: - - Template body. Use this to pass in the actual body of the CloudFormation template. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) - are specified, the previous template will be reused. - type: str - template_url: - description: - - 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 - as the stack. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) - are specified, the previous template will be reused. - type: str - purge_stacks: - description: - - Only applicable when I(state=absent). Sets whether, when deleting a stack set, the stack instances should also be deleted. - - By default, instances will be deleted. To keep stacks when stack set is deleted set I(purge_stacks=false). - type: bool - default: true - wait: - description: - - Whether or not to wait for stack operation to complete. This includes waiting for stack instances to reach UPDATE_COMPLETE status. - - If you choose not to wait, this module will not notify when stack operations fail because it will not wait for them to finish. - type: bool - default: false - wait_timeout: - description: - - How long to wait (in seconds) for stacks to complete create/update/delete operations. - default: 900 - type: int - capabilities: - description: - - Capabilities allow stacks to create and modify IAM resources, which may include adding users or roles. - - Currently the only available values are 'CAPABILITY_IAM' and 'CAPABILITY_NAMED_IAM'. Either or both may be provided. - - > - The following resources require that one or both of these parameters is specified: AWS::IAM::AccessKey, - AWS::IAM::Group, AWS::IAM::InstanceProfile, AWS::IAM::Policy, AWS::IAM::Role, AWS::IAM::User, AWS::IAM::UserToGroupAddition - type: list - elements: str - choices: - - 'CAPABILITY_IAM' - - 'CAPABILITY_NAMED_IAM' - regions: - description: - - 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) - specifies the region for stack instances. - - At least one region must be specified to create a stack set. On updates, if fewer regions are specified only the specified regions will - have their stack instances updated. - type: list - elements: str - accounts: - description: - - A list of AWS accounts in which to create instance of CloudFormation stacks. - - At least one region must be specified to create a stack set. On updates, if fewer regions are specified only the specified regions will - have their stack instances updated. - type: list - elements: str - administration_role_arn: - description: - - ARN of the administration role, meaning the role that CloudFormation Stack Sets use to assume the roles in your child accounts. - - This defaults to C(arn:aws:iam::{{ account ID }}:role/AWSCloudFormationStackSetAdministrationRole) where C({{ account ID }}) is replaced with the - account number of the current IAM role/user/STS credentials. - aliases: - - admin_role_arn - - admin_role - - administration_role - type: str - execution_role_name: - description: - - ARN of the execution role, meaning the role that CloudFormation Stack Sets assumes in your child accounts. - - This MUST NOT be an ARN, and the roles must exist in each child account specified. - - The default name for the execution role is C(AWSCloudFormationStackSetExecutionRole) - aliases: - - exec_role_name - - exec_role - - execution_role - type: str - tags: - description: - - Dictionary of tags to associate with stack and its resources during stack creation. - - Can be updated later, updating tags removes previous entries. - type: dict - failure_tolerance: - description: - - Settings to change what is considered "failed" when running stack instance updates, and how many to do at a time. - type: dict - suboptions: - fail_count: - description: - - The number of accounts, per region, for which this operation can fail before CloudFormation - stops the operation in that region. - - You must specify one of I(fail_count) and I(fail_percentage). - type: int - fail_percentage: - type: int - description: - - The percentage of accounts, per region, for which this stack operation can fail before CloudFormation - stops the operation in that region. - - You must specify one of I(fail_count) and I(fail_percentage). - parallel_percentage: - type: int - description: - - The maximum percentage of accounts in which to perform this operation at one time. - - You must specify one of I(parallel_count) and I(parallel_percentage). - - Note that this setting lets you specify the maximum for operations. - For large deployments, under certain circumstances the actual percentage may be lower. - parallel_count: - type: int - description: - - The maximum number of accounts in which to perform this operation at one time. - - I(parallel_count) may be at most one more than the I(fail_count). - - You must specify one of I(parallel_count) and I(parallel_percentage). - - Note that this setting lets you specify the maximum for operations. - For large deployments, under certain circumstances the actual count may be lower. - -author: "Ryan Scott Brown (@ryansb)" -extends_documentation_fragment: -- aws -- ec2 -requirements: [ boto3>=1.6, botocore>=1.10.26 ] -''' - -EXAMPLES = ''' -- name: Create a stack set with instances in two accounts - cloudformation_stack_set: - name: my-stack - description: Test stack in two accounts - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - accounts: [1234567890, 2345678901] - regions: - - us-east-1 - -- name: on subsequent calls, templates are optional but parameters and tags can be altered - cloudformation_stack_set: - name: my-stack - state: present - parameters: - InstanceName: my_stacked_instance - tags: - foo: bar - test: stack - accounts: [1234567890, 2345678901] - regions: - - us-east-1 - -- name: The same type of update, but wait for the update to complete in all stacks - cloudformation_stack_set: - name: my-stack - state: present - wait: true - parameters: - InstanceName: my_restacked_instance - tags: - foo: bar - test: stack - accounts: [1234567890, 2345678901] - regions: - - us-east-1 -''' - -RETURN = ''' -operations_log: - type: list - description: Most recent events in CloudFormation's event log. This may be from a previous run in some cases. - returned: always - sample: - - action: CREATE - creation_timestamp: '2018-06-18T17:40:46.372000+00:00' - end_timestamp: '2018-06-18T17:41:24.560000+00:00' - operation_id: Ansible-StackInstance-Create-0ff2af5b-251d-4fdb-8b89-1ee444eba8b8 - status: FAILED - stack_instances: - - account: '1234567890' - region: us-east-1 - stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929 - status: OUTDATED - status_reason: Account 1234567890 should have 'AWSCloudFormationStackSetAdministrationRole' role with trust relationship to CloudFormation service. - -operations: - description: All operations initiated by this run of the cloudformation_stack_set module - returned: always - type: list - sample: - - action: CREATE - administration_role_arn: arn:aws:iam::1234567890:role/AWSCloudFormationStackSetAdministrationRole - creation_timestamp: '2018-06-18T17:40:46.372000+00:00' - end_timestamp: '2018-06-18T17:41:24.560000+00:00' - execution_role_name: AWSCloudFormationStackSetExecutionRole - operation_id: Ansible-StackInstance-Create-0ff2af5b-251d-4fdb-8b89-1ee444eba8b8 - operation_preferences: - region_order: - - us-east-1 - - us-east-2 - stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929 - status: FAILED -stack_instances: - description: CloudFormation stack instances that are members of this stack set. This will also include their region and account ID. - returned: state == present - type: list - sample: - - account: '1234567890' - region: us-east-1 - stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929 - status: OUTDATED - status_reason: > - Account 1234567890 should have 'AWSCloudFormationStackSetAdministrationRole' role with trust relationship to CloudFormation service. - - account: '1234567890' - region: us-east-2 - stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929 - status: OUTDATED - status_reason: Cancelled since failure tolerance has exceeded -stack_set: - type: dict - description: Facts about the currently deployed stack set, its parameters, and its tags - returned: state == present - sample: - administration_role_arn: arn:aws:iam::1234567890:role/AWSCloudFormationStackSetAdministrationRole - capabilities: [] - description: test stack PRIME - execution_role_name: AWSCloudFormationStackSetExecutionRole - parameters: [] - stack_set_arn: arn:aws:cloudformation:us-east-1:1234567890:stackset/TestStackPrime:19f3f684-aae9-467-ba36-e09f92cf5929 - stack_set_id: TestStackPrime:19f3f684-aae9-4e67-ba36-e09f92cf5929 - stack_set_name: TestStackPrime - status: ACTIVE - tags: - Some: Thing - an: other - template_body: | - AWSTemplateFormatVersion: "2010-09-09" - Parameters: {} - Resources: - Bukkit: - Type: "AWS::S3::Bucket" - Properties: {} - other: - Type: "AWS::SNS::Topic" - Properties: {} - -''' # NOQA - -import time -import datetime -import uuid -import itertools - -try: - import boto3 - import botocore.exceptions - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - # handled by AnsibleAWSModule - pass - -from ansible.module_utils.ec2 import AWSRetry, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, camel_dict_to_snake_dict -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils._text import to_native - - -def create_stack_set(module, stack_params, cfn): - try: - cfn.create_stack_set(aws_retry=True, **stack_params) - return await_stack_set_exists(cfn, stack_params['StackSetName']) - except (ClientError, BotoCoreError) as err: - module.fail_json_aws(err, msg="Failed to create stack set {0}.".format(stack_params.get('StackSetName'))) - - -def update_stack_set(module, stack_params, cfn): - # if the state is present and the stack already exists, we try to update it. - # AWS will tell us if the stack template and parameters are the same and - # don't need to be updated. - try: - cfn.update_stack_set(**stack_params) - except is_boto3_error_code('StackSetNotFound') as err: # pylint: disable=duplicate-except - module.fail_json_aws(err, msg="Failed to find stack set. Check the name & region.") - except is_boto3_error_code('StackInstanceNotFound') as err: # pylint: disable=duplicate-except - module.fail_json_aws(err, msg="One or more stack instances were not found for this stack set. Double check " - "the `accounts` and `regions` parameters.") - except is_boto3_error_code('OperationInProgressException') as err: # pylint: disable=duplicate-except - module.fail_json_aws( - err, msg="Another operation is already in progress on this stack set - please try again later. When making " - "multiple cloudformation_stack_set calls, it's best to enable `wait: yes` to avoid unfinished op errors.") - except (ClientError, BotoCoreError) as err: # pylint: disable=duplicate-except - module.fail_json_aws(err, msg="Could not update stack set.") - if module.params.get('wait'): - await_stack_set_operation( - module, cfn, operation_id=stack_params['OperationId'], - stack_set_name=stack_params['StackSetName'], - max_wait=module.params.get('wait_timeout'), - ) - - return True - - -def compare_stack_instances(cfn, stack_set_name, accounts, regions): - instance_list = cfn.list_stack_instances( - aws_retry=True, - StackSetName=stack_set_name, - )['Summaries'] - desired_stack_instances = set(itertools.product(accounts, regions)) - existing_stack_instances = set((i['Account'], i['Region']) for i in instance_list) - # new stacks, existing stacks, unspecified stacks - return (desired_stack_instances - existing_stack_instances), existing_stack_instances, (existing_stack_instances - desired_stack_instances) - - -@AWSRetry.backoff(tries=3, delay=4) -def stack_set_facts(cfn, stack_set_name): - try: - ss = cfn.describe_stack_set(StackSetName=stack_set_name)['StackSet'] - ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags']) - return ss - except cfn.exceptions.from_code('StackSetNotFound'): - # Return None if the stack doesn't exist - return - - -def await_stack_set_operation(module, cfn, stack_set_name, operation_id, max_wait): - wait_start = datetime.datetime.now() - operation = None - for i in range(max_wait // 15): - try: - operation = cfn.describe_stack_set_operation(StackSetName=stack_set_name, OperationId=operation_id) - if operation['StackSetOperation']['Status'] not in ('RUNNING', 'STOPPING'): - # Stack set has completed operation - break - except is_boto3_error_code('StackSetNotFound'): # pylint: disable=duplicate-except - pass - except is_boto3_error_code('OperationNotFound'): # pylint: disable=duplicate-except - pass - time.sleep(15) - - if operation and operation['StackSetOperation']['Status'] not in ('FAILED', 'STOPPED'): - await_stack_instance_completion( - module, cfn, - stack_set_name=stack_set_name, - # subtract however long we waited already - max_wait=int(max_wait - (datetime.datetime.now() - wait_start).total_seconds()), - ) - elif operation and operation['StackSetOperation']['Status'] in ('FAILED', 'STOPPED'): - pass - else: - module.warn( - "Timed out waiting for operation {0} on stack set {1} after {2} seconds. Returning unfinished operation".format( - operation_id, stack_set_name, max_wait - ) - ) - - -def await_stack_instance_completion(module, cfn, stack_set_name, max_wait): - to_await = None - for i in range(max_wait // 15): - try: - stack_instances = cfn.list_stack_instances(StackSetName=stack_set_name) - to_await = [inst for inst in stack_instances['Summaries'] - if inst['Status'] != 'CURRENT'] - if not to_await: - return stack_instances['Summaries'] - except is_boto3_error_code('StackSetNotFound'): # pylint: disable=duplicate-except - # this means the deletion beat us, or the stack set is not yet propagated - pass - time.sleep(15) - - module.warn( - "Timed out waiting for stack set {0} instances {1} to complete after {2} seconds. Returning unfinished operation".format( - stack_set_name, ', '.join(s['StackId'] for s in to_await), max_wait - ) - ) - - -def await_stack_set_exists(cfn, stack_set_name): - # AWSRetry will retry on `StackSetNotFound` errors for us - ss = cfn.describe_stack_set(StackSetName=stack_set_name, aws_retry=True)['StackSet'] - ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags']) - return camel_dict_to_snake_dict(ss, ignore_list=('Tags',)) - - -def describe_stack_tree(module, stack_set_name, operation_ids=None): - jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=5, delay=3, max_delay=5, catch_extra_error_codes=['StackSetNotFound']) - cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator) - result = dict() - result['stack_set'] = camel_dict_to_snake_dict( - cfn.describe_stack_set( - StackSetName=stack_set_name, - aws_retry=True, - )['StackSet'] - ) - result['stack_set']['tags'] = boto3_tag_list_to_ansible_dict(result['stack_set']['tags']) - result['operations_log'] = sorted( - camel_dict_to_snake_dict( - cfn.list_stack_set_operations( - StackSetName=stack_set_name, - aws_retry=True, - ) - )['summaries'], - key=lambda x: x['creation_timestamp'] - ) - result['stack_instances'] = sorted( - [ - camel_dict_to_snake_dict(i) for i in - cfn.list_stack_instances(StackSetName=stack_set_name)['Summaries'] - ], - key=lambda i: i['region'] + i['account'] - ) - - if operation_ids: - result['operations'] = [] - for op_id in operation_ids: - try: - result['operations'].append(camel_dict_to_snake_dict( - cfn.describe_stack_set_operation( - StackSetName=stack_set_name, - OperationId=op_id, - )['StackSetOperation'] - )) - except is_boto3_error_code('OperationNotFoundException'): # pylint: disable=duplicate-except - pass - return result - - -def get_operation_preferences(module): - params = dict() - if module.params.get('regions'): - params['RegionOrder'] = list(module.params['regions']) - for param, api_name in { - 'fail_count': 'FailureToleranceCount', - 'fail_percentage': 'FailureTolerancePercentage', - 'parallel_percentage': 'MaxConcurrentPercentage', - 'parallel_count': 'MaxConcurrentCount', - }.items(): - if module.params.get('failure_tolerance', {}).get(param): - params[api_name] = module.params.get('failure_tolerance', {}).get(param) - return params - - -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 - jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=10, delay=3, max_delay=30, catch_extra_error_codes=['StackSetNotFound']) - cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator) - 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 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_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_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) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py b/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py deleted file mode 100644 index a2d2514a35..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py +++ /dev/null @@ -1,2264 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- - -module: cloudfront_distribution - -short_description: Create, update and delete AWS CloudFront distributions. - -description: - - Allows for easy creation, updating and deletion of CloudFront distributions. - -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 - -version_added: "2.5" - -author: - - Willem van Ketwich (@wilvk) - - Will Thames (@willthames) - -extends_documentation_fragment: - - aws - - ec2 - -options: - - state: - description: - - The desired state of the distribution. - - I(state=present) creates a new distribution or updates an existing distribution. - - I(state=absent) deletes an existing distribution. - choices: ['present', 'absent'] - default: 'present' - type: str - - distribution_id: - description: - - The ID of the CloudFront distribution. - - This parameter can be exchanged with I(alias) or I(caller_reference) and is used in conjunction with I(e_tag). - type: str - - e_tag: - description: - - A unique identifier of a modified or existing distribution. Used in conjunction with I(distribution_id). - - Is determined automatically if not specified. - type: str - - caller_reference: - description: - - A unique identifier for creating and updating CloudFront distributions. - - Each caller reference must be unique across all distributions. e.g. a caller reference used in a web - distribution cannot be reused in a streaming distribution. This parameter can be used instead of I(distribution_id) - to reference an existing distribution. If not specified, this defaults to a datetime stamp of the format - C(YYYY-MM-DDTHH:MM:SS.ffffff). - type: str - - tags: - description: - - Should be input as a dict of key-value pairs. - - Note that numeric keys or values must be wrapped in quotes. e.g. "Priority:" '1' - type: dict - - purge_tags: - description: - - Specifies whether existing tags will be removed before adding new tags. - - When I(purge_tags=yes), existing tags are removed and I(tags) are added, if specified. - If no tags are specified, it removes all existing tags for the distribution. - - When I(purge_tags=no), existing tags are kept and I(tags) are added, if specified. - default: false - type: bool - - alias: - description: - - The name of an alias (CNAME) that is used in a distribution. This is used to effectively reference a distribution by its alias as an alias can only - be used by one distribution per AWS account. This variable avoids having to provide the I(distribution_id) as well as - the I(e_tag), or I(caller_reference) of an existing distribution. - type: str - - aliases: - description: - - A list) of domain name aliases (CNAMEs) as strings to be used for the distribution. - - Each alias must be unique across all distribution for the AWS account. - type: list - elements: str - - purge_aliases: - description: - - Specifies whether existing aliases will be removed before adding new aliases. - - When I(purge_aliases=yes), existing aliases are removed and I(aliases) are added. - default: false - type: bool - - default_root_object: - description: - - A config element that specifies the path to request when the user requests the origin. - - e.g. if specified as 'index.html', this maps to www.example.com/index.html when www.example.com is called by the user. - - This prevents the entire distribution origin from being exposed at the root. - type: str - - default_origin_domain_name: - description: - - The domain name to use for an origin if no I(origins) have been specified. - - Should only be used on a first run of generating a distribution and not on - subsequent runs. - - Should not be used in conjunction with I(distribution_id), I(caller_reference) or I(alias). - type: str - - default_origin_path: - description: - - The default origin path to specify for an origin if no I(origins) have been specified. Defaults to empty if not specified. - type: str - - origins: - type: list - elements: dict - description: - - A config element that is a list of complex origin objects to be specified for the distribution. Used for creating and updating distributions. - suboptions: - id: - description: A unique identifier for the origin or origin group. I(id) must be unique within the distribution. - type: str - domain_name: - description: - - The domain name which CloudFront will query as the origin. - - For more information see the CloudFront documentation - at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesDomainName) - type: str - origin_path: - description: Tells CloudFront to request your content from a directory in your Amazon S3 bucket or your custom origin. - type: str - custom_headers: - description: - - Custom headers you wish to add to the request before passing it to the origin. - - For more information see the CloudFront documentation - at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html) - type: list - elements: dict - suboptions: - header_name: - description: The name of a header that you want CloudFront to forward to your origin. - type: str - header_value: - description: The value for the header that you specified in the I(header_name) field. - type: str - s3_origin_access_identity_enabled: - description: - - Use an origin access identity to configure the origin so that viewers can only access objects in an Amazon S3 bucket through CloudFront. - - Will automatically create an Identity for you. - - See also U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html). - type: bool - custom_origin_config: - description: Connection information about the origin. - type: dict - suboptions: - http_port: - description: The HTTP port the custom origin listens on. - type: int - https_port: - description: The HTTPS port the custom origin listens on. - type: int - origin_protocol_policy: - description: The origin protocol policy to apply to your origin. - type: str - origin_ssl_protocols: - description: A list of SSL/TLS protocols that you want CloudFront to use when communicating to the origin over HTTPS. - type: list - elements: str - origin_read_timeout: - description: A timeout (in seconds) when reading from your origin. - type: int - origin_keepalive_timeout: - description: A keep-alive timeout (in seconds). - type: int - - purge_origins: - description: Whether to remove any origins that aren't listed in I(origins). - default: false - type: bool - - default_cache_behavior: - type: dict - description: - - A dict specifying the default cache behavior of the distribution. - - If not specified, the I(target_origin_id) is defined as the I(target_origin_id) of the first valid - I(cache_behavior) in I(cache_behaviors) with defaults. - suboptions: - target_origin_id: - description: - - The ID of the origin that you want CloudFront to route requests to - by default. - type: str - forwarded_values: - description: - - A dict that specifies how CloudFront handles query strings and cookies. - type: dict - suboptions: - query_string: - description: - - Indicates whether you want CloudFront to forward query strings - to the origin that is associated with this cache behavior. - type: bool - cookies: - description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones. - type: dict - suboptions: - forward: - description: - - Specifies which cookies to forward to the origin for this cache behavior. - - Valid values are C(all), C(none), or C(whitelist). - type: str - whitelisted_names: - type: list - elements: str - description: A list of coockies to forward to the origin for this cache behavior. - headers: - description: - - A list of headers to forward to the origin for this cache behavior. - - To forward all headers use a list containing a single element '*' (C(['*'])) - type: list - elements: str - query_string_cache_keys: - description: - - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior. - type: list - elements: str - trusted_signers: - description: - - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content. - type: dict - suboptions: - enabled: - description: Whether you want to require viewers to use signed URLs to access the files specified by I(target_origin_id) - type: bool - items: - description: A list of trusted signers for this cache behavior. - elements: str - type: list - viewer_protocol_policy: - description: - - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id). - - Valid values are C(allow-all), C(redirect-to-https) and C(https-only). - type: str - default_ttl: - description: The default amount of time that you want objects to stay in CloudFront caches. - type: int - max_ttl: - description: The maximum amount of time that you want objects to stay in CloudFront caches. - type: int - min_ttl: - description: The minimum amount of time that you want objects to stay in CloudFront caches. - type: int - allowed_methods: - description: A dict that controls which HTTP methods CloudFront processes and forwards. - type: dict - suboptions: - items: - description: A list of HTTP methods that you want CloudFront to process and forward. - type: list - elements: str - cached_methods: - description: - - A list of HTTP methods that you want CloudFront to apply caching to. - - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]). - type: list - elements: str - smooth_streaming: - description: - - Whether you want to distribute media files in the Microsoft Smooth Streaming format. - type: bool - compress: - description: - - Whether you want CloudFront to automatically compress files. - type: bool - lambda_function_associations: - description: - - A list of Lambda function associations to use for this cache behavior. - type: list - elements: dict - suboptions: - lambda_function_arn: - description: The ARN of the Lambda function. - type: str - event_type: - description: - - Specifies the event type that triggers a Lambda function invocation. - - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response). - type: str - field_level_encryption_id: - description: - - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data. - type: str - - cache_behaviors: - type: list - elements: dict - description: - - A list of dictionaries describing the cache behaviors for the distribution. - - The order of the list is preserved across runs unless I(purge_cache_behaviors) is enabled. - suboptions: - path_pattern: - description: - - The pattern that specifies which requests to apply the behavior to. - type: str - target_origin_id: - description: - - The ID of the origin that you want CloudFront to route requests to - by default. - type: str - forwarded_values: - description: - - A dict that specifies how CloudFront handles query strings and cookies. - type: dict - suboptions: - query_string: - description: - - Indicates whether you want CloudFront to forward query strings - to the origin that is associated with this cache behavior. - type: bool - cookies: - description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones. - type: dict - suboptions: - forward: - description: - - Specifies which cookies to forward to the origin for this cache behavior. - - Valid values are C(all), C(none), or C(whitelist). - type: str - whitelisted_names: - type: list - elements: str - description: A list of coockies to forward to the origin for this cache behavior. - headers: - description: - - A list of headers to forward to the origin for this cache behavior. - - To forward all headers use a list containing a single element '*' (C(['*'])) - type: list - elements: str - query_string_cache_keys: - description: - - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior. - type: list - elements: str - trusted_signers: - description: - - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content. - type: dict - suboptions: - enabled: - description: Whether you want to require viewers to use signed URLs to access the files specified by I(path_pattern) and I(target_origin_id) - type: bool - items: - description: A list of trusted signers for this cache behavior. - elements: str - type: list - viewer_protocol_policy: - description: - - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id) when a request matches I(path_pattern). - - Valid values are C(allow-all), C(redirect-to-https) and C(https-only). - type: str - default_ttl: - description: The default amount of time that you want objects to stay in CloudFront caches. - type: int - max_ttl: - description: The maximum amount of time that you want objects to stay in CloudFront caches. - type: int - min_ttl: - description: The minimum amount of time that you want objects to stay in CloudFront caches. - type: int - allowed_methods: - description: A dict that controls which HTTP methods CloudFront processes and forwards. - type: dict - suboptions: - items: - description: A list of HTTP methods that you want CloudFront to process and forward. - type: list - elements: str - cached_methods: - description: - - A list of HTTP methods that you want CloudFront to apply caching to. - - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]). - type: list - elements: str - smooth_streaming: - description: - - Whether you want to distribute media files in the Microsoft Smooth Streaming format. - type: bool - compress: - description: - - Whether you want CloudFront to automatically compress files. - type: bool - lambda_function_associations: - description: - - A list of Lambda function associations to use for this cache behavior. - type: list - elements: dict - suboptions: - lambda_function_arn: - description: The ARN of the Lambda function. - type: str - event_type: - description: - - Specifies the event type that triggers a Lambda function invocation. - - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response). - type: str - field_level_encryption_id: - description: - - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data. - type: str - - - purge_cache_behaviors: - description: - - Whether to remove any cache behaviors that aren't listed in I(cache_behaviors). - - This switch also allows the reordering of I(cache_behaviors). - default: false - type: bool - - custom_error_responses: - type: list - elements: dict - description: - - A config element that is a I(list[]) of complex custom error responses to be specified for the distribution. - - This attribute configures custom http error messages returned to the user. - suboptions: - error_code: - type: int - description: The error code the custom error page is for. - error_caching_min_ttl: - type: int - description: The length of time (in seconds) that CloudFront will cache status codes for. - response_code: - type: int - description: - - The HTTP status code that CloudFront should return to a user when the origin returns the HTTP status code specified by I(error_code). - response_page_path: - type: str - description: - - The path to the custom error page that you want CloudFront to return to a viewer when your origin returns - the HTTP status code specified by I(error_code). - - purge_custom_error_responses: - description: Whether to remove any custom error responses that aren't listed in I(custom_error_responses). - default: false - type: bool - - comment: - description: - - A comment that describes the CloudFront distribution. - - If not specified, it defaults to a generic message that it has been created with Ansible, and a datetime stamp. - type: str - - logging: - description: - - A config element that is a complex object that defines logging for the distribution. - suboptions: - enabled: - description: When I(enabled=true) CloudFront will log access to an S3 bucket. - type: bool - include_cookies: - description: When I(include_cookies=true) CloudFront will include cookies in the logs. - type: bool - bucket: - description: The S3 bucket to store the log in. - type: str - prefix: - description: A prefix to include in the S3 object names. - type: str - type: dict - - price_class: - description: - - A string that specifies the pricing class of the distribution. As per - U(https://aws.amazon.com/cloudfront/pricing/) - - I(price_class=PriceClass_100) consists of the areas United States, Canada and Europe. - - I(price_class=PriceClass_200) consists of the areas United States, Canada, Europe, Japan, India, - Hong Kong, Philippines, S. Korea, Singapore & Taiwan. - - I(price_class=PriceClass_All) consists of the areas United States, Canada, Europe, Japan, India, - South America, Australia, Hong Kong, Philippines, S. Korea, Singapore & Taiwan. - - AWS defaults this to C(PriceClass_All). - - Valid values are C(PriceClass_100), C(PriceClass_200) and C(PriceClass_All) - type: str - - enabled: - description: - - A boolean value that specifies whether the distribution is enabled or disabled. - default: false - type: bool - - viewer_certificate: - type: dict - description: - - A dict that specifies the encryption details of the distribution. - suboptions: - cloudfront_default_certificate: - type: bool - description: - - If you're using the CloudFront domain name for your distribution, such as C(123456789abcde.cloudfront.net) - you should set I(cloudfront_default_certificate=true) - - If I(cloudfront_default_certificate=true) do not set I(ssl_support_method). - iam_certificate_id: - type: str - description: - - The ID of a certificate stored in IAM to use for HTTPS connections. - - If I(iam_certificate_id) is set then you must also specify I(ssl_support_method) - acm_certificate_arn: - type: str - description: - - The ID of a certificate stored in ACM to use for HTTPS connections. - - If I(acm_certificate_id) is set then you must also specify I(ssl_support_method) - ssl_support_method: - type: str - description: - - How CloudFront should serve SSL certificates. - - Valid values are C(sni-only) for SNI, and C(vip) if CloudFront is configured to use a dedicated IP for your content. - minimum_protocol_version: - type: str - description: - - The security policy that you want CloudFront to use for HTTPS connections. - - See U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html) - for supported security policies. - - restrictions: - type: dict - description: - - A config element that is a complex object that describes how a distribution should restrict it's content. - suboptions: - geo_restriction: - description: Apply a restriciton based on the location of the requester. - type: dict - suboptions: - restriction_type: - type: str - description: - - The method that you want to use to restrict distribution of your content by country. - - Valid values are C(none), C(whitelist), C(blacklist) - items: - description: - - A list of ISO 3166-1 two letter (Alpha 2) country codes that the - restriction should apply to. - - 'See the ISO website for a full list of codes U(https://www.iso.org/obp/ui/#search/code/)' - type: list - - web_acl_id: - description: - - The ID of a Web Application Firewall (WAF) Access Control List (ACL). - type: str - - http_version: - description: - - The version of the http protocol to use for the distribution. - - AWS defaults this to C(http2). - - Valid values are C(http1.1) and C(http2) - type: str - - ipv6_enabled: - description: - - Determines whether IPv6 support is enabled or not. - type: bool - default: false - - wait: - description: - - Specifies whether the module waits until the distribution has completed processing the creation or update. - type: bool - default: false - - wait_timeout: - description: - - Specifies the duration in seconds to wait for a timeout of a cloudfront create or update. - default: 1800 - type: int - -''' - -EXAMPLES = ''' - -# create a basic distribution with defaults and tags - -- cloudfront_distribution: - state: present - default_origin_domain_name: www.my-cloudfront-origin.com - tags: - Name: example distribution - Project: example project - Priority: '1' - -# update a distribution comment by distribution_id - -- cloudfront_distribution: - state: present - distribution_id: E1RP5A2MJ8073O - comment: modified by ansible cloudfront.py - -# update a distribution comment by caller_reference - -- cloudfront_distribution: - state: present - caller_reference: my cloudfront distribution 001 - comment: modified by ansible cloudfront.py - -# update a distribution's aliases and comment using the distribution_id as a reference - -- cloudfront_distribution: - state: present - distribution_id: E1RP5A2MJ8073O - comment: modified by cloudfront.py again - aliases: [ 'www.my-distribution-source.com', 'zzz.aaa.io' ] - -# update a distribution's aliases and comment using an alias as a reference - -- cloudfront_distribution: - state: present - caller_reference: my test distribution - comment: modified by cloudfront.py again - aliases: - - www.my-distribution-source.com - - zzz.aaa.io - -# update a distribution's comment and aliases and tags and remove existing tags - -- cloudfront_distribution: - state: present - distribution_id: E15BU8SDCGSG57 - comment: modified by cloudfront.py again - aliases: - - tested.com - tags: - Project: distribution 1.2 - purge_tags: yes - -# create a distribution with an origin, logging and default cache behavior - -- cloudfront_distribution: - state: present - caller_reference: unique test distribution ID - origins: - - id: 'my test origin-000111' - domain_name: www.example.com - origin_path: /production - custom_headers: - - header_name: MyCustomHeaderName - header_value: MyCustomHeaderValue - default_cache_behavior: - target_origin_id: 'my test origin-000111' - forwarded_values: - query_string: true - cookies: - forward: all - headers: - - '*' - viewer_protocol_policy: allow-all - smooth_streaming: true - compress: true - allowed_methods: - items: - - GET - - HEAD - cached_methods: - - GET - - HEAD - logging: - enabled: true - include_cookies: false - bucket: mylogbucket.s3.amazonaws.com - prefix: myprefix/ - enabled: false - comment: this is a CloudFront distribution with logging - -# delete a distribution - -- cloudfront_distribution: - state: absent - caller_reference: replaceable distribution -''' - -RETURN = ''' -active_trusted_signers: - description: Key pair IDs that CloudFront is aware of for each trusted signer. - returned: always - type: complex - contains: - enabled: - description: Whether trusted signers are in use. - returned: always - type: bool - sample: false - quantity: - description: Number of trusted signers. - returned: always - type: int - sample: 1 - items: - description: Number of trusted signers. - returned: when there are trusted signers - type: list - sample: - - key_pair_id -aliases: - description: Aliases that refer to the distribution. - returned: always - type: complex - contains: - items: - description: List of aliases. - returned: always - type: list - sample: - - test.example.com - quantity: - description: Number of aliases. - returned: always - type: int - sample: 1 -arn: - description: Amazon Resource Name of the distribution. - returned: always - type: str - sample: arn:aws:cloudfront::123456789012:distribution/E1234ABCDEFGHI -cache_behaviors: - description: CloudFront cache behaviors. - returned: always - type: complex - contains: - items: - description: List of cache behaviors. - returned: always - type: complex - contains: - allowed_methods: - description: Methods allowed by the cache behavior. - returned: always - type: complex - contains: - cached_methods: - description: Methods cached by the cache behavior. - returned: always - type: complex - contains: - items: - description: List of cached methods. - returned: always - type: list - sample: - - HEAD - - GET - quantity: - description: Count of cached methods. - returned: always - type: int - sample: 2 - items: - description: List of methods allowed by the cache behavior. - returned: always - type: list - sample: - - HEAD - - GET - quantity: - description: Count of methods allowed by the cache behavior. - returned: always - type: int - sample: 2 - compress: - description: Whether compression is turned on for the cache behavior. - returned: always - type: bool - sample: false - default_ttl: - description: Default Time to Live of the cache behavior. - returned: always - type: int - sample: 86400 - forwarded_values: - description: Values forwarded to the origin for this cache behavior. - returned: always - type: complex - contains: - cookies: - description: Cookies to forward to the origin. - returned: always - type: complex - contains: - forward: - description: Which cookies to forward to the origin for this cache behavior. - returned: always - type: str - sample: none - whitelisted_names: - description: The names of the cookies to forward to the origin for this cache behavior. - returned: when I(forward=whitelist) - type: complex - contains: - quantity: - description: Count of cookies to forward. - returned: always - type: int - sample: 1 - items: - description: List of cookies to forward. - returned: when list is not empty - type: list - sample: my_cookie - headers: - description: Which headers are used to vary on cache retrievals. - returned: always - type: complex - contains: - quantity: - description: Count of headers to vary on. - returned: always - type: int - sample: 1 - items: - description: List of headers to vary on. - returned: when list is not empty - type: list - sample: - - Host - query_string: - description: Whether the query string is used in cache lookups. - returned: always - type: bool - sample: false - query_string_cache_keys: - description: Which query string keys to use in cache lookups. - returned: always - type: complex - contains: - quantity: - description: Count of query string cache keys to use in cache lookups. - returned: always - type: int - sample: 1 - items: - description: List of query string cache keys to use in cache lookups. - returned: when list is not empty - type: list - sample: - lambda_function_associations: - description: Lambda function associations for a cache behavior. - returned: always - type: complex - contains: - quantity: - description: Count of lambda function associations. - returned: always - type: int - sample: 1 - items: - description: List of lambda function associations. - returned: when list is not empty - type: list - sample: - - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function - event_type: viewer-response - max_ttl: - description: Maximum Time to Live. - returned: always - type: int - sample: 31536000 - min_ttl: - description: Minimum Time to Live. - returned: always - type: int - sample: 0 - path_pattern: - description: Path pattern that determines this cache behavior. - returned: always - type: str - sample: /path/to/files/* - smooth_streaming: - description: Whether smooth streaming is enabled. - returned: always - type: bool - sample: false - target_origin_id: - description: ID of origin reference by this cache behavior. - returned: always - type: str - sample: origin_abcd - trusted_signers: - description: Trusted signers. - returned: always - type: complex - contains: - enabled: - description: Whether trusted signers are enabled for this cache behavior. - returned: always - type: bool - sample: false - quantity: - description: Count of trusted signers. - returned: always - type: int - sample: 1 - viewer_protocol_policy: - description: Policy of how to handle http/https. - returned: always - type: str - sample: redirect-to-https - quantity: - description: Count of cache behaviors. - returned: always - type: int - sample: 1 - -caller_reference: - description: Idempotency reference given when creating CloudFront distribution. - returned: always - type: str - sample: '1484796016700' -comment: - description: Any comments you want to include about the distribution. - returned: always - type: str - sample: 'my first CloudFront distribution' -custom_error_responses: - description: Custom error responses to use for error handling. - returned: always - type: complex - contains: - items: - description: List of custom error responses. - returned: always - type: complex - contains: - error_caching_min_ttl: - description: Minimum time to cache this error response. - returned: always - type: int - sample: 300 - error_code: - description: Origin response code that triggers this error response. - returned: always - type: int - sample: 500 - response_code: - description: Response code to return to the requester. - returned: always - type: str - sample: '500' - response_page_path: - description: Path that contains the error page to display. - returned: always - type: str - sample: /errors/5xx.html - quantity: - description: Count of custom error response items - returned: always - type: int - sample: 1 -default_cache_behavior: - description: Default cache behavior. - returned: always - type: complex - contains: - allowed_methods: - description: Methods allowed by the cache behavior. - returned: always - type: complex - contains: - cached_methods: - description: Methods cached by the cache behavior. - returned: always - type: complex - contains: - items: - description: List of cached methods. - returned: always - type: list - sample: - - HEAD - - GET - quantity: - description: Count of cached methods. - returned: always - type: int - sample: 2 - items: - description: List of methods allowed by the cache behavior. - returned: always - type: list - sample: - - HEAD - - GET - quantity: - description: Count of methods allowed by the cache behavior. - returned: always - type: int - sample: 2 - compress: - description: Whether compression is turned on for the cache behavior. - returned: always - type: bool - sample: false - default_ttl: - description: Default Time to Live of the cache behavior. - returned: always - type: int - sample: 86400 - forwarded_values: - description: Values forwarded to the origin for this cache behavior. - returned: always - type: complex - contains: - cookies: - description: Cookies to forward to the origin. - returned: always - type: complex - contains: - forward: - description: Which cookies to forward to the origin for this cache behavior. - returned: always - type: str - sample: none - whitelisted_names: - description: The names of the cookies to forward to the origin for this cache behavior. - returned: when I(forward=whitelist) - type: complex - contains: - quantity: - description: Count of cookies to forward. - returned: always - type: int - sample: 1 - items: - description: List of cookies to forward. - returned: when list is not empty - type: list - sample: my_cookie - headers: - description: Which headers are used to vary on cache retrievals. - returned: always - type: complex - contains: - quantity: - description: Count of headers to vary on. - returned: always - type: int - sample: 1 - items: - description: List of headers to vary on. - returned: when list is not empty - type: list - sample: - - Host - query_string: - description: Whether the query string is used in cache lookups. - returned: always - type: bool - sample: false - query_string_cache_keys: - description: Which query string keys to use in cache lookups. - returned: always - type: complex - contains: - quantity: - description: Count of query string cache keys to use in cache lookups. - returned: always - type: int - sample: 1 - items: - description: List of query string cache keys to use in cache lookups. - returned: when list is not empty - type: list - sample: - lambda_function_associations: - description: Lambda function associations for a cache behavior. - returned: always - type: complex - contains: - quantity: - description: Count of lambda function associations. - returned: always - type: int - sample: 1 - items: - description: List of lambda function associations. - returned: when list is not empty - type: list - sample: - - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function - event_type: viewer-response - max_ttl: - description: Maximum Time to Live. - returned: always - type: int - sample: 31536000 - min_ttl: - description: Minimum Time to Live. - returned: always - type: int - sample: 0 - path_pattern: - description: Path pattern that determines this cache behavior. - returned: always - type: str - sample: /path/to/files/* - smooth_streaming: - description: Whether smooth streaming is enabled. - returned: always - type: bool - sample: false - target_origin_id: - description: ID of origin reference by this cache behavior. - returned: always - type: str - sample: origin_abcd - trusted_signers: - description: Trusted signers. - returned: always - type: complex - contains: - enabled: - description: Whether trusted signers are enabled for this cache behavior. - returned: always - type: bool - sample: false - quantity: - description: Count of trusted signers. - returned: always - type: int - sample: 1 - viewer_protocol_policy: - description: Policy of how to handle http/https. - returned: always - type: str - sample: redirect-to-https -default_root_object: - description: The object that you want CloudFront to request from your origin (for example, index.html) - when a viewer requests the root URL for your distribution. - returned: always - type: str - sample: '' -diff: - description: Difference between previous configuration and new configuration. - returned: always - type: dict - sample: {} -domain_name: - description: Domain name of CloudFront distribution. - returned: always - type: str - sample: d1vz8pzgurxosf.cloudfront.net -enabled: - description: Whether the CloudFront distribution is enabled or not. - returned: always - type: bool - sample: true -http_version: - description: Version of HTTP supported by the distribution. - returned: always - type: str - sample: http2 -id: - description: CloudFront distribution ID. - returned: always - type: str - sample: E123456ABCDEFG -in_progress_invalidation_batches: - description: The number of invalidation batches currently in progress. - returned: always - type: int - sample: 0 -is_ipv6_enabled: - description: Whether IPv6 is enabled. - returned: always - type: bool - sample: true -last_modified_time: - description: Date and time distribution was last modified. - returned: always - type: str - sample: '2017-10-13T01:51:12.656000+00:00' -logging: - description: Logging information. - returned: always - type: complex - contains: - bucket: - description: S3 bucket logging destination. - returned: always - type: str - sample: logs-example-com.s3.amazonaws.com - enabled: - description: Whether logging is enabled. - returned: always - type: bool - sample: true - include_cookies: - description: Whether to log cookies. - returned: always - type: bool - sample: false - prefix: - description: Prefix added to logging object names. - returned: always - type: str - sample: cloudfront/test -origins: - description: Origins in the CloudFront distribution. - returned: always - type: complex - contains: - items: - description: List of origins. - returned: always - type: complex - contains: - custom_headers: - description: Custom headers passed to the origin. - returned: always - type: complex - contains: - quantity: - description: Count of headers. - returned: always - type: int - sample: 1 - custom_origin_config: - description: Configuration of the origin. - returned: always - type: complex - contains: - http_port: - description: Port on which HTTP is listening. - returned: always - type: int - sample: 80 - https_port: - description: Port on which HTTPS is listening. - returned: always - type: int - sample: 443 - origin_keepalive_timeout: - description: Keep-alive timeout. - returned: always - type: int - sample: 5 - origin_protocol_policy: - description: Policy of which protocols are supported. - returned: always - type: str - sample: https-only - origin_read_timeout: - description: Timeout for reads to the origin. - returned: always - type: int - sample: 30 - origin_ssl_protocols: - description: SSL protocols allowed by the origin. - returned: always - type: complex - contains: - items: - description: List of SSL protocols. - returned: always - type: list - sample: - - TLSv1 - - TLSv1.1 - - TLSv1.2 - quantity: - description: Count of SSL protocols. - returned: always - type: int - sample: 3 - domain_name: - description: Domain name of the origin. - returned: always - type: str - sample: test-origin.example.com - id: - description: ID of the origin. - returned: always - type: str - sample: test-origin.example.com - origin_path: - description: Subdirectory to prefix the request from the S3 or HTTP origin. - returned: always - type: str - sample: '' - quantity: - description: Count of origins. - returned: always - type: int - sample: 1 -price_class: - description: Price class of CloudFront distribution. - returned: always - type: str - sample: PriceClass_All -restrictions: - description: Restrictions in use by CloudFront. - returned: always - type: complex - contains: - geo_restriction: - description: Controls the countries in which your content is distributed. - returned: always - type: complex - contains: - quantity: - description: Count of restrictions. - returned: always - type: int - sample: 1 - items: - description: List of country codes allowed or disallowed. - returned: always - type: list - sample: xy - restriction_type: - description: Type of restriction. - returned: always - type: str - sample: blacklist -status: - description: Status of the CloudFront distribution. - returned: always - type: str - sample: InProgress -tags: - description: Distribution tags. - returned: always - type: dict - sample: - Hello: World -viewer_certificate: - description: Certificate used by CloudFront distribution. - returned: always - type: complex - contains: - acm_certificate_arn: - description: ARN of ACM certificate. - returned: when certificate comes from ACM - type: str - sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef - certificate: - description: Reference to certificate. - returned: always - type: str - sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef - certificate_source: - description: Where certificate comes from. - returned: always - type: str - sample: acm - minimum_protocol_version: - description: Minimum SSL/TLS protocol supported by this distribution. - returned: always - type: str - sample: TLSv1 - ssl_support_method: - description: Support for pre-SNI browsers or not. - returned: always - type: str - sample: sni-only -web_acl_id: - description: ID of Web Access Control List (from WAF service). - returned: always - type: str - sample: abcd1234-1234-abcd-abcd-abcd12345678 -''' - -from ansible.module_utils._text import to_text, to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager -from ansible.module_utils.common.dict_transformations import recursive_diff -from ansible.module_utils.ec2 import compare_aws_tags, ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict -import datetime - -try: - from collections import OrderedDict -except ImportError: - try: - from ordereddict import OrderedDict - except ImportError: - pass # caught by AnsibleAWSModule (as python 2.6 + boto3 => ordereddict is installed) - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -def change_dict_key_name(dictionary, old_key, new_key): - if old_key in dictionary: - dictionary[new_key] = dictionary.get(old_key) - dictionary.pop(old_key, None) - return dictionary - - -def merge_validation_into_config(config, validated_node, node_name): - if validated_node is not None: - if isinstance(validated_node, dict): - config_node = config.get(node_name) - if config_node is not None: - config_node_items = list(config_node.items()) - else: - config_node_items = [] - config[node_name] = dict(config_node_items + list(validated_node.items())) - if isinstance(validated_node, list): - config[node_name] = list(set(config.get(node_name) + validated_node)) - return config - - -def ansible_list_to_cloudfront_list(list_items=None, include_quantity=True): - if list_items is None: - list_items = [] - if not isinstance(list_items, list): - raise ValueError('Expected a list, got a {0} with value {1}'.format(type(list_items).__name__, str(list_items))) - result = {} - if include_quantity: - result['quantity'] = len(list_items) - if len(list_items) > 0: - result['items'] = list_items - return result - - -def create_distribution(client, module, config, tags): - try: - if not tags: - return client.create_distribution(DistributionConfig=config)['Distribution'] - else: - distribution_config_with_tags = { - 'DistributionConfig': config, - 'Tags': { - 'Items': tags - } - } - return client.create_distribution_with_tags(DistributionConfigWithTags=distribution_config_with_tags)['Distribution'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error creating distribution") - - -def delete_distribution(client, module, distribution): - try: - return client.delete_distribution(Id=distribution['Distribution']['Id'], IfMatch=distribution['ETag']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error deleting distribution %s" % to_native(distribution['Distribution'])) - - -def update_distribution(client, module, config, distribution_id, e_tag): - try: - return client.update_distribution(DistributionConfig=config, Id=distribution_id, IfMatch=e_tag)['Distribution'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error updating distribution to %s" % to_native(config)) - - -def tag_resource(client, module, arn, tags): - try: - return client.tag_resource(Resource=arn, Tags=dict(Items=tags)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error tagging resource") - - -def untag_resource(client, module, arn, tag_keys): - try: - return client.untag_resource(Resource=arn, TagKeys=dict(Items=tag_keys)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error untagging resource") - - -def list_tags_for_resource(client, module, arn): - try: - response = client.list_tags_for_resource(Resource=arn) - return boto3_tag_list_to_ansible_dict(response.get('Tags').get('Items')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error listing tags for resource") - - -def update_tags(client, module, existing_tags, valid_tags, purge_tags, arn): - changed = False - to_add, to_remove = compare_aws_tags(existing_tags, valid_tags, purge_tags) - if to_remove: - untag_resource(client, module, arn, to_remove) - changed = True - if to_add: - tag_resource(client, module, arn, ansible_dict_to_boto3_tag_list(to_add)) - changed = True - return changed - - -class CloudFrontValidationManager(object): - """ - Manages CloudFront validations - """ - - def __init__(self, module): - self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module) - self.module = module - self.__default_distribution_enabled = True - self.__default_http_port = 80 - self.__default_https_port = 443 - self.__default_ipv6_enabled = False - self.__default_origin_ssl_protocols = [ - 'TLSv1', - 'TLSv1.1', - 'TLSv1.2' - ] - self.__default_custom_origin_protocol_policy = 'match-viewer' - self.__default_custom_origin_read_timeout = 30 - self.__default_custom_origin_keepalive_timeout = 5 - self.__default_datetime_string = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') - self.__default_cache_behavior_min_ttl = 0 - self.__default_cache_behavior_max_ttl = 31536000 - self.__default_cache_behavior_default_ttl = 86400 - self.__default_cache_behavior_compress = False - self.__default_cache_behavior_viewer_protocol_policy = 'allow-all' - self.__default_cache_behavior_smooth_streaming = False - self.__default_cache_behavior_forwarded_values_forward_cookies = 'none' - self.__default_cache_behavior_forwarded_values_query_string = True - self.__default_trusted_signers_enabled = False - self.__valid_price_classes = set([ - 'PriceClass_100', - 'PriceClass_200', - 'PriceClass_All' - ]) - self.__valid_origin_protocol_policies = set([ - 'http-only', - 'match-viewer', - 'https-only' - ]) - self.__valid_origin_ssl_protocols = set([ - 'SSLv3', - 'TLSv1', - 'TLSv1.1', - 'TLSv1.2' - ]) - self.__valid_cookie_forwarding = set([ - 'none', - 'whitelist', - 'all' - ]) - self.__valid_viewer_protocol_policies = set([ - 'allow-all', - 'https-only', - 'redirect-to-https' - ]) - self.__valid_methods = set([ - 'GET', - 'HEAD', - 'POST', - 'PUT', - 'PATCH', - 'OPTIONS', - 'DELETE' - ]) - self.__valid_methods_cached_methods = [ - set([ - 'GET', - 'HEAD' - ]), - set([ - 'GET', - 'HEAD', - 'OPTIONS' - ]) - ] - self.__valid_methods_allowed_methods = [ - self.__valid_methods_cached_methods[0], - self.__valid_methods_cached_methods[1], - self.__valid_methods - ] - self.__valid_lambda_function_association_event_types = set([ - 'viewer-request', - 'viewer-response', - 'origin-request', - 'origin-response' - ]) - self.__valid_viewer_certificate_ssl_support_methods = set([ - 'sni-only', - 'vip' - ]) - self.__valid_viewer_certificate_minimum_protocol_versions = set([ - 'SSLv3', - 'TLSv1', - 'TLSv1_2016', - 'TLSv1.1_2016', - 'TLSv1.2_2018' - ]) - self.__valid_viewer_certificate_certificate_sources = set([ - 'cloudfront', - 'iam', - 'acm' - ]) - self.__valid_http_versions = set([ - 'http1.1', - 'http2' - ]) - self.__s3_bucket_domain_identifier = '.s3.amazonaws.com' - - def add_missing_key(self, dict_object, key_to_set, value_to_set): - if key_to_set not in dict_object and value_to_set is not None: - dict_object[key_to_set] = value_to_set - return dict_object - - def add_key_else_change_dict_key(self, dict_object, old_key, new_key, value_to_set): - if old_key not in dict_object and value_to_set is not None: - dict_object[new_key] = value_to_set - else: - dict_object = change_dict_key_name(dict_object, old_key, new_key) - return dict_object - - def add_key_else_validate(self, dict_object, key_name, attribute_name, value_to_set, valid_values, to_aws_list=False): - if key_name in dict_object: - self.validate_attribute_with_allowed_values(value_to_set, attribute_name, valid_values) - else: - if to_aws_list: - dict_object[key_name] = ansible_list_to_cloudfront_list(value_to_set) - elif value_to_set is not None: - dict_object[key_name] = value_to_set - return dict_object - - def validate_logging(self, logging): - try: - if logging is None: - return None - valid_logging = {} - if logging and not set(['enabled', 'include_cookies', 'bucket', 'prefix']).issubset(logging): - self.module.fail_json(msg="The logging parameters enabled, include_cookies, bucket and prefix must be specified.") - valid_logging['include_cookies'] = logging.get('include_cookies') - valid_logging['enabled'] = logging.get('enabled') - valid_logging['bucket'] = logging.get('bucket') - valid_logging['prefix'] = logging.get('prefix') - return valid_logging - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution logging") - - def validate_is_list(self, list_to_validate, list_name): - if not isinstance(list_to_validate, list): - self.module.fail_json(msg='%s is of type %s. Must be a list.' % (list_name, type(list_to_validate).__name__)) - - def validate_required_key(self, key_name, full_key_name, dict_object): - if key_name not in dict_object: - self.module.fail_json(msg="%s must be specified." % full_key_name) - - def validate_origins(self, client, config, origins, default_origin_domain_name, - default_origin_path, create_distribution, purge_origins=False): - try: - if origins is None: - if default_origin_domain_name is None and not create_distribution: - if purge_origins: - return None - else: - return ansible_list_to_cloudfront_list(config) - if default_origin_domain_name is not None: - origins = [{ - 'domain_name': default_origin_domain_name, - 'origin_path': default_origin_path or '' - }] - else: - origins = [] - self.validate_is_list(origins, 'origins') - if not origins and default_origin_domain_name is None and create_distribution: - self.module.fail_json(msg="Both origins[] and default_origin_domain_name have not been specified. Please specify at least one.") - all_origins = OrderedDict() - new_domains = list() - for origin in config: - all_origins[origin.get('domain_name')] = origin - for origin in origins: - origin = self.validate_origin(client, all_origins.get(origin.get('domain_name'), {}), origin, default_origin_path) - all_origins[origin['domain_name']] = origin - new_domains.append(origin['domain_name']) - if purge_origins: - for domain in list(all_origins.keys()): - if domain not in new_domains: - del(all_origins[domain]) - return ansible_list_to_cloudfront_list(list(all_origins.values())) - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution origins") - - def validate_s3_origin_configuration(self, client, existing_config, origin): - if origin['s3_origin_access_identity_enabled'] and existing_config.get('s3_origin_config', {}).get('origin_access_identity'): - return existing_config['s3_origin_config']['origin_access_identity'] - if not origin['s3_origin_access_identity_enabled']: - return None - try: - comment = "access-identity-by-ansible-%s-%s" % (origin.get('domain_name'), self.__default_datetime_string) - caller_reference = "%s-%s" % (origin.get('domain_name'), self.__default_datetime_string) - cfoai_config = dict(CloudFrontOriginAccessIdentityConfig=dict(CallerReference=caller_reference, - Comment=comment)) - oai = client.create_cloud_front_origin_access_identity(**cfoai_config)['CloudFrontOriginAccessIdentity']['Id'] - except Exception as e: - self.module.fail_json_aws(e, msg="Couldn't create Origin Access Identity for id %s" % origin['id']) - return "origin-access-identity/cloudfront/%s" % oai - - def validate_origin(self, client, existing_config, origin, default_origin_path): - try: - origin = self.add_missing_key(origin, 'origin_path', existing_config.get('origin_path', default_origin_path or '')) - self.validate_required_key('origin_path', 'origins[].origin_path', origin) - origin = self.add_missing_key(origin, 'id', existing_config.get('id', self.__default_datetime_string)) - if 'custom_headers' in origin and len(origin.get('custom_headers')) > 0: - for custom_header in origin.get('custom_headers'): - if 'header_name' not in custom_header or 'header_value' not in custom_header: - self.module.fail_json(msg="Both origins[].custom_headers.header_name and origins[].custom_headers.header_value must be specified.") - origin['custom_headers'] = ansible_list_to_cloudfront_list(origin.get('custom_headers')) - else: - origin['custom_headers'] = ansible_list_to_cloudfront_list() - if self.__s3_bucket_domain_identifier in origin.get('domain_name').lower(): - if origin.get("s3_origin_access_identity_enabled") is not None: - s3_origin_config = self.validate_s3_origin_configuration(client, existing_config, origin) - if s3_origin_config: - oai = s3_origin_config - else: - oai = "" - origin["s3_origin_config"] = dict(origin_access_identity=oai) - del(origin["s3_origin_access_identity_enabled"]) - if 'custom_origin_config' in origin: - self.module.fail_json(msg="s3_origin_access_identity_enabled and custom_origin_config are mutually exclusive") - else: - origin = self.add_missing_key(origin, 'custom_origin_config', existing_config.get('custom_origin_config', {})) - custom_origin_config = origin.get('custom_origin_config') - custom_origin_config = self.add_key_else_validate(custom_origin_config, 'origin_protocol_policy', - 'origins[].custom_origin_config.origin_protocol_policy', - self.__default_custom_origin_protocol_policy, self.__valid_origin_protocol_policies) - custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_read_timeout', self.__default_custom_origin_read_timeout) - custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_keepalive_timeout', self.__default_custom_origin_keepalive_timeout) - custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'http_port', 'h_t_t_p_port', self.__default_http_port) - custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'https_port', 'h_t_t_p_s_port', self.__default_https_port) - if custom_origin_config.get('origin_ssl_protocols', {}).get('items'): - custom_origin_config['origin_ssl_protocols'] = custom_origin_config['origin_ssl_protocols']['items'] - if custom_origin_config.get('origin_ssl_protocols'): - self.validate_attribute_list_with_allowed_list(custom_origin_config['origin_ssl_protocols'], 'origins[].origin_ssl_protocols', - self.__valid_origin_ssl_protocols) - else: - custom_origin_config['origin_ssl_protocols'] = self.__default_origin_ssl_protocols - custom_origin_config['origin_ssl_protocols'] = ansible_list_to_cloudfront_list(custom_origin_config['origin_ssl_protocols']) - return origin - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error validating distribution origin") - - def validate_cache_behaviors(self, config, cache_behaviors, valid_origins, purge_cache_behaviors=False): - try: - if cache_behaviors is None and valid_origins is not None and purge_cache_behaviors is False: - return ansible_list_to_cloudfront_list(config) - all_cache_behaviors = OrderedDict() - # cache behaviors are order dependent so we don't preserve the existing ordering when purge_cache_behaviors - # is true (if purge_cache_behaviors is not true, we can't really know the full new order) - if not purge_cache_behaviors: - for behavior in config: - all_cache_behaviors[behavior['path_pattern']] = behavior - for cache_behavior in cache_behaviors: - valid_cache_behavior = self.validate_cache_behavior(all_cache_behaviors.get(cache_behavior.get('path_pattern'), {}), - cache_behavior, valid_origins) - all_cache_behaviors[cache_behavior['path_pattern']] = valid_cache_behavior - if purge_cache_behaviors: - for target_origin_id in set(all_cache_behaviors.keys()) - set([cb['path_pattern'] for cb in cache_behaviors]): - del(all_cache_behaviors[target_origin_id]) - return ansible_list_to_cloudfront_list(list(all_cache_behaviors.values())) - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution cache behaviors") - - def validate_cache_behavior(self, config, cache_behavior, valid_origins, is_default_cache=False): - if is_default_cache and cache_behavior is None: - cache_behavior = {} - if cache_behavior is None and valid_origins is not None: - return config - cache_behavior = self.validate_cache_behavior_first_level_keys(config, cache_behavior, valid_origins, is_default_cache) - cache_behavior = self.validate_forwarded_values(config, cache_behavior.get('forwarded_values'), cache_behavior) - cache_behavior = self.validate_allowed_methods(config, cache_behavior.get('allowed_methods'), cache_behavior) - cache_behavior = self.validate_lambda_function_associations(config, cache_behavior.get('lambda_function_associations'), cache_behavior) - cache_behavior = self.validate_trusted_signers(config, cache_behavior.get('trusted_signers'), cache_behavior) - cache_behavior = self.validate_field_level_encryption_id(config, cache_behavior.get('field_level_encryption_id'), cache_behavior) - return cache_behavior - - def validate_cache_behavior_first_level_keys(self, config, cache_behavior, valid_origins, is_default_cache): - try: - cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'min_ttl', 'min_t_t_l', - config.get('min_t_t_l', self.__default_cache_behavior_min_ttl)) - cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'max_ttl', 'max_t_t_l', - config.get('max_t_t_l', self.__default_cache_behavior_max_ttl)) - cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'default_ttl', 'default_t_t_l', - config.get('default_t_t_l', self.__default_cache_behavior_default_ttl)) - cache_behavior = self.add_missing_key(cache_behavior, 'compress', config.get('compress', self.__default_cache_behavior_compress)) - target_origin_id = cache_behavior.get('target_origin_id', config.get('target_origin_id')) - if not target_origin_id: - target_origin_id = self.get_first_origin_id_for_default_cache_behavior(valid_origins) - if target_origin_id not in [origin['id'] for origin in valid_origins.get('items', [])]: - if is_default_cache: - cache_behavior_name = 'Default cache behavior' - else: - cache_behavior_name = 'Cache behavior for path %s' % cache_behavior['path_pattern'] - self.module.fail_json(msg="%s has target_origin_id pointing to an origin that does not exist." % - cache_behavior_name) - cache_behavior['target_origin_id'] = target_origin_id - cache_behavior = self.add_key_else_validate(cache_behavior, 'viewer_protocol_policy', 'cache_behavior.viewer_protocol_policy', - config.get('viewer_protocol_policy', - self.__default_cache_behavior_viewer_protocol_policy), - self.__valid_viewer_protocol_policies) - cache_behavior = self.add_missing_key(cache_behavior, 'smooth_streaming', - config.get('smooth_streaming', self.__default_cache_behavior_smooth_streaming)) - return cache_behavior - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution cache behavior first level keys") - - def validate_forwarded_values(self, config, forwarded_values, cache_behavior): - try: - if not forwarded_values: - forwarded_values = dict() - existing_config = config.get('forwarded_values', {}) - headers = forwarded_values.get('headers', existing_config.get('headers', {}).get('items')) - if headers: - headers.sort() - forwarded_values['headers'] = ansible_list_to_cloudfront_list(headers) - if 'cookies' not in forwarded_values: - forward = existing_config.get('cookies', {}).get('forward', self.__default_cache_behavior_forwarded_values_forward_cookies) - forwarded_values['cookies'] = {'forward': forward} - else: - existing_whitelist = existing_config.get('cookies', {}).get('whitelisted_names', {}).get('items') - whitelist = forwarded_values.get('cookies').get('whitelisted_names', existing_whitelist) - if whitelist: - self.validate_is_list(whitelist, 'forwarded_values.whitelisted_names') - forwarded_values['cookies']['whitelisted_names'] = ansible_list_to_cloudfront_list(whitelist) - cookie_forwarding = forwarded_values.get('cookies').get('forward', existing_config.get('cookies', {}).get('forward')) - self.validate_attribute_with_allowed_values(cookie_forwarding, 'cache_behavior.forwarded_values.cookies.forward', - self.__valid_cookie_forwarding) - forwarded_values['cookies']['forward'] = cookie_forwarding - query_string_cache_keys = forwarded_values.get('query_string_cache_keys', existing_config.get('query_string_cache_keys', {}).get('items', [])) - self.validate_is_list(query_string_cache_keys, 'forwarded_values.query_string_cache_keys') - forwarded_values['query_string_cache_keys'] = ansible_list_to_cloudfront_list(query_string_cache_keys) - forwarded_values = self.add_missing_key(forwarded_values, 'query_string', - existing_config.get('query_string', self.__default_cache_behavior_forwarded_values_query_string)) - cache_behavior['forwarded_values'] = forwarded_values - return cache_behavior - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating forwarded values") - - def validate_lambda_function_associations(self, config, lambda_function_associations, cache_behavior): - try: - if lambda_function_associations is not None: - self.validate_is_list(lambda_function_associations, 'lambda_function_associations') - for association in lambda_function_associations: - association = change_dict_key_name(association, 'lambda_function_arn', 'lambda_function_a_r_n') - self.validate_attribute_with_allowed_values(association.get('event_type'), 'cache_behaviors[].lambda_function_associations.event_type', - self.__valid_lambda_function_association_event_types) - cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list(lambda_function_associations) - else: - if 'lambda_function_associations' in config: - cache_behavior['lambda_function_associations'] = config.get('lambda_function_associations') - else: - cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list([]) - return cache_behavior - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating lambda function associations") - - def validate_field_level_encryption_id(self, config, field_level_encryption_id, cache_behavior): - if field_level_encryption_id is not None: - cache_behavior['field_level_encryption_id'] = field_level_encryption_id - elif 'field_level_encryption_id' in config: - cache_behavior['field_level_encryption_id'] = config.get('field_level_encryption_id') - else: - cache_behavior['field_level_encryption_id'] = "" - return cache_behavior - - def validate_allowed_methods(self, config, allowed_methods, cache_behavior): - try: - if allowed_methods is not None: - self.validate_required_key('items', 'cache_behavior.allowed_methods.items[]', allowed_methods) - temp_allowed_items = allowed_methods.get('items') - self.validate_is_list(temp_allowed_items, 'cache_behavior.allowed_methods.items') - self.validate_attribute_list_with_allowed_list(temp_allowed_items, 'cache_behavior.allowed_methods.items[]', - self.__valid_methods_allowed_methods) - cached_items = allowed_methods.get('cached_methods') - if 'cached_methods' in allowed_methods: - self.validate_is_list(cached_items, 'cache_behavior.allowed_methods.cached_methods') - self.validate_attribute_list_with_allowed_list(cached_items, 'cache_behavior.allowed_items.cached_methods[]', - self.__valid_methods_cached_methods) - # we don't care if the order of how cloudfront stores the methods differs - preserving existing - # order reduces likelihood of making unnecessary changes - if 'allowed_methods' in config and set(config['allowed_methods']['items']) == set(temp_allowed_items): - cache_behavior['allowed_methods'] = config['allowed_methods'] - else: - cache_behavior['allowed_methods'] = ansible_list_to_cloudfront_list(temp_allowed_items) - - if cached_items and set(cached_items) == set(config.get('allowed_methods', {}).get('cached_methods', {}).get('items', [])): - cache_behavior['allowed_methods']['cached_methods'] = config['allowed_methods']['cached_methods'] - else: - cache_behavior['allowed_methods']['cached_methods'] = ansible_list_to_cloudfront_list(cached_items) - else: - if 'allowed_methods' in config: - cache_behavior['allowed_methods'] = config.get('allowed_methods') - return cache_behavior - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating allowed methods") - - def validate_trusted_signers(self, config, trusted_signers, cache_behavior): - try: - if trusted_signers is None: - trusted_signers = {} - if 'items' in trusted_signers: - valid_trusted_signers = ansible_list_to_cloudfront_list(trusted_signers.get('items')) - else: - valid_trusted_signers = dict(quantity=config.get('quantity', 0)) - if 'items' in config: - valid_trusted_signers = dict(items=config['items']) - valid_trusted_signers['enabled'] = trusted_signers.get('enabled', config.get('enabled', self.__default_trusted_signers_enabled)) - cache_behavior['trusted_signers'] = valid_trusted_signers - return cache_behavior - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating trusted signers") - - def validate_viewer_certificate(self, viewer_certificate): - try: - if viewer_certificate is None: - return None - if viewer_certificate.get('cloudfront_default_certificate') and viewer_certificate.get('ssl_support_method') is not None: - self.module.fail_json(msg="viewer_certificate.ssl_support_method should not be specified with viewer_certificate_cloudfront_default" + - "_certificate set to true.") - self.validate_attribute_with_allowed_values(viewer_certificate.get('ssl_support_method'), 'viewer_certificate.ssl_support_method', - self.__valid_viewer_certificate_ssl_support_methods) - self.validate_attribute_with_allowed_values(viewer_certificate.get('minimum_protocol_version'), 'viewer_certificate.minimum_protocol_version', - self.__valid_viewer_certificate_minimum_protocol_versions) - self.validate_attribute_with_allowed_values(viewer_certificate.get('certificate_source'), 'viewer_certificate.certificate_source', - self.__valid_viewer_certificate_certificate_sources) - viewer_certificate = change_dict_key_name(viewer_certificate, 'cloudfront_default_certificate', 'cloud_front_default_certificate') - viewer_certificate = change_dict_key_name(viewer_certificate, 'ssl_support_method', 's_s_l_support_method') - viewer_certificate = change_dict_key_name(viewer_certificate, 'iam_certificate_id', 'i_a_m_certificate_id') - viewer_certificate = change_dict_key_name(viewer_certificate, 'acm_certificate_arn', 'a_c_m_certificate_arn') - return viewer_certificate - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating viewer certificate") - - def validate_custom_error_responses(self, config, custom_error_responses, purge_custom_error_responses): - try: - if custom_error_responses is None and not purge_custom_error_responses: - return ansible_list_to_cloudfront_list(config) - self.validate_is_list(custom_error_responses, 'custom_error_responses') - result = list() - existing_responses = dict((response['error_code'], response) for response in custom_error_responses) - for custom_error_response in custom_error_responses: - self.validate_required_key('error_code', 'custom_error_responses[].error_code', custom_error_response) - custom_error_response = change_dict_key_name(custom_error_response, 'error_caching_min_ttl', 'error_caching_min_t_t_l') - if 'response_code' in custom_error_response: - custom_error_response['response_code'] = str(custom_error_response['response_code']) - if custom_error_response['error_code'] in existing_responses: - del(existing_responses[custom_error_response['error_code']]) - result.append(custom_error_response) - if not purge_custom_error_responses: - result.extend(existing_responses.values()) - - return ansible_list_to_cloudfront_list(result) - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating custom error responses") - - def validate_restrictions(self, config, restrictions, purge_restrictions=False): - try: - if restrictions is None: - if purge_restrictions: - return None - else: - return config - self.validate_required_key('geo_restriction', 'restrictions.geo_restriction', restrictions) - geo_restriction = restrictions.get('geo_restriction') - self.validate_required_key('restriction_type', 'restrictions.geo_restriction.restriction_type', geo_restriction) - existing_restrictions = config.get('geo_restriction', {}).get(geo_restriction['restriction_type'], {}).get('items', []) - geo_restriction_items = geo_restriction.get('items') - if not purge_restrictions: - geo_restriction_items.extend([rest for rest in existing_restrictions if - rest not in geo_restriction_items]) - valid_restrictions = ansible_list_to_cloudfront_list(geo_restriction_items) - valid_restrictions['restriction_type'] = geo_restriction.get('restriction_type') - return {'geo_restriction': valid_restrictions} - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating restrictions") - - def validate_distribution_config_parameters(self, config, default_root_object, ipv6_enabled, http_version, web_acl_id): - try: - config['default_root_object'] = default_root_object or config.get('default_root_object', '') - config['is_i_p_v_6_enabled'] = ipv6_enabled or config.get('i_p_v_6_enabled', self.__default_ipv6_enabled) - if http_version is not None or config.get('http_version'): - self.validate_attribute_with_allowed_values(http_version, 'http_version', self.__valid_http_versions) - config['http_version'] = http_version or config.get('http_version') - if web_acl_id or config.get('web_a_c_l_id'): - config['web_a_c_l_id'] = web_acl_id or config.get('web_a_c_l_id') - return config - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution config parameters") - - def validate_common_distribution_parameters(self, config, enabled, aliases, logging, price_class, purge_aliases=False): - try: - if config is None: - config = {} - if aliases is not None: - if not purge_aliases: - aliases.extend([alias for alias in config.get('aliases', {}).get('items', []) - if alias not in aliases]) - config['aliases'] = ansible_list_to_cloudfront_list(aliases) - if logging is not None: - config['logging'] = self.validate_logging(logging) - config['enabled'] = enabled or config.get('enabled', self.__default_distribution_enabled) - if price_class is not None: - self.validate_attribute_with_allowed_values(price_class, 'price_class', self.__valid_price_classes) - config['price_class'] = price_class - return config - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating common distribution parameters") - - def validate_comment(self, config, comment): - config['comment'] = comment or config.get('comment', "Distribution created by Ansible with datetime stamp " + self.__default_datetime_string) - return config - - def validate_caller_reference(self, caller_reference): - return caller_reference or self.__default_datetime_string - - def get_first_origin_id_for_default_cache_behavior(self, valid_origins): - try: - if valid_origins is not None: - valid_origins_list = valid_origins.get('items') - if valid_origins_list is not None and isinstance(valid_origins_list, list) and len(valid_origins_list) > 0: - return str(valid_origins_list[0].get('id')) - self.module.fail_json(msg="There are no valid origins from which to specify a target_origin_id for the default_cache_behavior configuration.") - except Exception as e: - self.module.fail_json_aws(e, msg="Error getting first origin_id for default cache behavior") - - def validate_attribute_list_with_allowed_list(self, attribute_list, attribute_list_name, allowed_list): - try: - self.validate_is_list(attribute_list, attribute_list_name) - if (isinstance(allowed_list, list) and set(attribute_list) not in allowed_list or - isinstance(allowed_list, set) and not set(allowed_list).issuperset(attribute_list)): - self.module.fail_json(msg='The attribute list {0} must be one of [{1}]'.format(attribute_list_name, ' '.join(str(a) for a in allowed_list))) - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating attribute list with allowed value list") - - def validate_attribute_with_allowed_values(self, attribute, attribute_name, allowed_list): - if attribute is not None and attribute not in allowed_list: - self.module.fail_json(msg='The attribute {0} must be one of [{1}]'.format(attribute_name, ' '.join(str(a) for a in allowed_list))) - - def validate_distribution_from_caller_reference(self, caller_reference): - try: - distributions = self.__cloudfront_facts_mgr.list_distributions(False) - distribution_name = 'Distribution' - distribution_config_name = 'DistributionConfig' - distribution_ids = [dist.get('Id') for dist in distributions] - for distribution_id in distribution_ids: - distribution = self.__cloudfront_facts_mgr.get_distribution(distribution_id) - if distribution is not None: - distribution_config = distribution[distribution_name].get(distribution_config_name) - if distribution_config is not None and distribution_config.get('CallerReference') == caller_reference: - distribution[distribution_name][distribution_config_name] = distribution_config - return distribution - - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution from caller reference") - - def validate_distribution_from_aliases_caller_reference(self, distribution_id, aliases, caller_reference): - try: - if caller_reference is not None: - return self.validate_distribution_from_caller_reference(caller_reference) - else: - if aliases: - distribution_id = self.validate_distribution_id_from_alias(aliases) - if distribution_id: - return self.__cloudfront_facts_mgr.get_distribution(distribution_id) - return None - except Exception as e: - self.module.fail_json_aws(e, msg="Error validating distribution_id from alias, aliases and caller reference") - - def validate_distribution_id_from_alias(self, aliases): - distributions = self.__cloudfront_facts_mgr.list_distributions(False) - if distributions: - for distribution in distributions: - distribution_aliases = distribution.get('Aliases', {}).get('Items', []) - if set(aliases) & set(distribution_aliases): - return distribution['Id'] - return None - - def wait_until_processed(self, client, wait_timeout, distribution_id, caller_reference): - if distribution_id is None: - distribution_id = self.validate_distribution_from_caller_reference(caller_reference=caller_reference)['Id'] - - try: - waiter = client.get_waiter('distribution_deployed') - attempts = 1 + int(wait_timeout / 60) - waiter.wait(Id=distribution_id, WaiterConfig={'MaxAttempts': attempts}) - except botocore.exceptions.WaiterError as e: - self.module.fail_json_aws(e, msg="Timeout waiting for CloudFront action." - " Waited for {0} seconds before timeout.".format(to_text(wait_timeout))) - - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error getting distribution {0}".format(distribution_id)) - - -def main(): - argument_spec = dict( - state=dict(choices=['present', 'absent'], default='present'), - caller_reference=dict(), - comment=dict(), - distribution_id=dict(), - e_tag=dict(), - tags=dict(type='dict', default={}), - purge_tags=dict(type='bool', default=False), - alias=dict(), - aliases=dict(type='list', default=[]), - purge_aliases=dict(type='bool', default=False), - default_root_object=dict(), - origins=dict(type='list'), - purge_origins=dict(type='bool', default=False), - default_cache_behavior=dict(type='dict'), - cache_behaviors=dict(type='list'), - purge_cache_behaviors=dict(type='bool', default=False), - custom_error_responses=dict(type='list'), - purge_custom_error_responses=dict(type='bool', default=False), - logging=dict(type='dict'), - price_class=dict(), - enabled=dict(type='bool'), - viewer_certificate=dict(type='dict'), - restrictions=dict(type='dict'), - web_acl_id=dict(), - http_version=dict(), - ipv6_enabled=dict(type='bool'), - default_origin_domain_name=dict(), - default_origin_path=dict(), - wait=dict(default=False, type='bool'), - wait_timeout=dict(default=1800, type='int') - ) - - result = {} - changed = True - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=False, - mutually_exclusive=[ - ['distribution_id', 'alias'], - ['default_origin_domain_name', 'distribution_id'], - ['default_origin_domain_name', 'alias'], - ] - ) - - client = module.client('cloudfront') - - validation_mgr = CloudFrontValidationManager(module) - - state = module.params.get('state') - caller_reference = module.params.get('caller_reference') - comment = module.params.get('comment') - e_tag = module.params.get('e_tag') - tags = module.params.get('tags') - purge_tags = module.params.get('purge_tags') - distribution_id = module.params.get('distribution_id') - alias = module.params.get('alias') - aliases = module.params.get('aliases') - purge_aliases = module.params.get('purge_aliases') - default_root_object = module.params.get('default_root_object') - origins = module.params.get('origins') - purge_origins = module.params.get('purge_origins') - default_cache_behavior = module.params.get('default_cache_behavior') - cache_behaviors = module.params.get('cache_behaviors') - purge_cache_behaviors = module.params.get('purge_cache_behaviors') - custom_error_responses = module.params.get('custom_error_responses') - purge_custom_error_responses = module.params.get('purge_custom_error_responses') - logging = module.params.get('logging') - price_class = module.params.get('price_class') - enabled = module.params.get('enabled') - viewer_certificate = module.params.get('viewer_certificate') - restrictions = module.params.get('restrictions') - purge_restrictions = module.params.get('purge_restrictions') - web_acl_id = module.params.get('web_acl_id') - http_version = module.params.get('http_version') - ipv6_enabled = module.params.get('ipv6_enabled') - default_origin_domain_name = module.params.get('default_origin_domain_name') - default_origin_path = module.params.get('default_origin_path') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - if alias and alias not in aliases: - aliases.append(alias) - - distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference) - - update = state == 'present' and distribution - create = state == 'present' and not distribution - delete = state == 'absent' and distribution - - if not (update or create or delete): - module.exit_json(changed=False) - - if update or delete: - config = distribution['Distribution']['DistributionConfig'] - e_tag = distribution['ETag'] - distribution_id = distribution['Distribution']['Id'] - else: - config = dict() - if update: - config = camel_dict_to_snake_dict(config, reversible=True) - - if create or update: - config = validation_mgr.validate_common_distribution_parameters(config, enabled, aliases, logging, price_class, purge_aliases) - config = validation_mgr.validate_distribution_config_parameters(config, default_root_object, ipv6_enabled, http_version, web_acl_id) - config['origins'] = validation_mgr.validate_origins(client, config.get('origins', {}).get('items', []), origins, default_origin_domain_name, - default_origin_path, create, purge_origins) - config['cache_behaviors'] = validation_mgr.validate_cache_behaviors(config.get('cache_behaviors', {}).get('items', []), - cache_behaviors, config['origins'], purge_cache_behaviors) - config['default_cache_behavior'] = validation_mgr.validate_cache_behavior(config.get('default_cache_behavior', {}), - default_cache_behavior, config['origins'], True) - config['custom_error_responses'] = validation_mgr.validate_custom_error_responses(config.get('custom_error_responses', {}).get('items', []), - custom_error_responses, purge_custom_error_responses) - valid_restrictions = validation_mgr.validate_restrictions(config.get('restrictions', {}), restrictions, purge_restrictions) - if valid_restrictions: - config['restrictions'] = valid_restrictions - valid_viewer_certificate = validation_mgr.validate_viewer_certificate(viewer_certificate) - config = merge_validation_into_config(config, valid_viewer_certificate, 'viewer_certificate') - config = validation_mgr.validate_comment(config, comment) - config = snake_dict_to_camel_dict(config, capitalize_first=True) - - if create: - config['CallerReference'] = validation_mgr.validate_caller_reference(caller_reference) - result = create_distribution(client, module, config, ansible_dict_to_boto3_tag_list(tags)) - result = camel_dict_to_snake_dict(result) - result['tags'] = list_tags_for_resource(client, module, result['arn']) - - if delete: - if config['Enabled']: - config['Enabled'] = False - result = update_distribution(client, module, config, distribution_id, e_tag) - validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference')) - distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference) - # e_tag = distribution['ETag'] - result = delete_distribution(client, module, distribution) - - if update: - changed = config != distribution['Distribution']['DistributionConfig'] - if changed: - result = update_distribution(client, module, config, distribution_id, e_tag) - else: - result = distribution['Distribution'] - existing_tags = list_tags_for_resource(client, module, result['ARN']) - distribution['Distribution']['DistributionConfig']['tags'] = existing_tags - changed |= update_tags(client, module, existing_tags, tags, purge_tags, result['ARN']) - result = camel_dict_to_snake_dict(result) - result['distribution_config']['tags'] = config['tags'] = list_tags_for_resource(client, module, result['arn']) - result['diff'] = dict() - diff = recursive_diff(distribution['Distribution']['DistributionConfig'], config) - if diff: - result['diff']['before'] = diff[0] - result['diff']['after'] = diff[1] - - if wait and (create or update): - validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference')) - - if 'distribution_config' in result: - result.update(result['distribution_config']) - del(result['distribution_config']) - - module.exit_json(changed=changed, **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_info.py b/lib/ansible/modules/cloud/amazon/cloudfront_info.py deleted file mode 100644 index 4845cf779d..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudfront_info.py +++ /dev/null @@ -1,729 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudfront_info -short_description: Obtain facts about an AWS CloudFront distribution -description: - - Gets information about an AWS CloudFront distribution. - - This module was called C(cloudfront_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(cloudfront_info) module no longer returns C(ansible_facts)! -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 -version_added: "2.3" -author: Willem van Ketwich (@wilvk) -options: - distribution_id: - description: - - The id of the CloudFront distribution. Used with I(distribution), I(distribution_config), - I(invalidation), I(streaming_distribution), I(streaming_distribution_config), I(list_invalidations). - required: false - type: str - invalidation_id: - description: - - The id of the invalidation to get information about. - - Used with I(invalidation). - required: false - type: str - origin_access_identity_id: - description: - - The id of the CloudFront origin access identity to get information about. - required: false - type: str -# web_acl_id: -# description: -# - Used with I(list_distributions_by_web_acl_id). -# required: false -# type: str - domain_name_alias: - description: - - Can be used instead of I(distribution_id) - uses the aliased CNAME for the CloudFront - distribution to get the distribution id where required. - required: false - type: str - all_lists: - description: - - Get all CloudFront lists that do not require parameters. - required: false - default: false - type: bool - origin_access_identity: - description: - - Get information about an origin access identity. - - Requires I(origin_access_identity_id) to be specified. - required: false - default: false - type: bool - origin_access_identity_config: - description: - - Get the configuration information about an origin access identity. - - Requires I(origin_access_identity_id) to be specified. - required: false - default: false - type: bool - distribution: - description: - - Get information about a distribution. - - Requires I(distribution_id) or I(domain_name_alias) to be specified. - required: false - default: false - type: bool - distribution_config: - description: - - Get the configuration information about a distribution. - - Requires I(distribution_id) or I(domain_name_alias) to be specified. - required: false - default: false - type: bool - invalidation: - description: - - Get information about an invalidation. - - Requires I(invalidation_id) to be specified. - required: false - default: false - type: bool - streaming_distribution: - description: - - Get information about a specified RTMP distribution. - - Requires I(distribution_id) or I(domain_name_alias) to be specified. - required: false - default: false - type: bool - streaming_distribution_config: - description: - - Get the configuration information about a specified RTMP distribution. - - Requires I(distribution_id) or I(domain_name_alias) to be specified. - required: false - default: false - type: bool - list_origin_access_identities: - description: - - Get a list of CloudFront origin access identities. - - Requires I(origin_access_identity_id) to be set. - required: false - default: false - type: bool - list_distributions: - description: - - Get a list of CloudFront distributions. - required: false - default: false - type: bool - list_distributions_by_web_acl_id: - description: - - Get a list of distributions using web acl id as a filter. - - Requires I(web_acl_id) to be set. - required: false - default: false - type: bool - list_invalidations: - description: - - Get a list of invalidations. - - Requires I(distribution_id) or I(domain_name_alias) to be specified. - required: false - default: false - type: bool - list_streaming_distributions: - description: - - Get a list of streaming distributions. - required: false - default: false - type: bool - summary: - description: - - Returns a summary of all distributions, streaming distributions and origin_access_identities. - - This is the default behaviour if no option is selected. - required: false - default: false - type: bool - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Get a summary of distributions -- cloudfront_info: - summary: true - register: result - -# Get information about a distribution -- cloudfront_info: - distribution: true - distribution_id: my-cloudfront-distribution-id - register: result_did -- debug: - msg: "{{ result_did['cloudfront']['my-cloudfront-distribution-id'] }}" - -# Get information about a distribution using the CNAME of the cloudfront distribution. -- cloudfront_info: - distribution: true - domain_name_alias: www.my-website.com - register: result_website -- debug: - msg: "{{ result_website['cloudfront']['www.my-website.com'] }}" - -# When the module is called as cloudfront_facts, return values are published -# in ansible_facts['cloudfront'][<id>] and can be used as follows. -# Note that this is deprecated and will stop working in Ansible 2.13. -- cloudfront_facts: - distribution: true - distribution_id: my-cloudfront-distribution-id -- debug: - msg: "{{ ansible_facts['cloudfront']['my-cloudfront-distribution-id'] }}" - -- cloudfront_facts: - distribution: true - domain_name_alias: www.my-website.com -- debug: - msg: "{{ ansible_facts['cloudfront']['www.my-website.com'] }}" - -# Get all information about an invalidation for a distribution. -- cloudfront_facts: - invalidation: true - distribution_id: my-cloudfront-distribution-id - invalidation_id: my-cloudfront-invalidation-id - -# Get all information about a CloudFront origin access identity. -- cloudfront_facts: - origin_access_identity: true - origin_access_identity_id: my-cloudfront-origin-access-identity-id - -# Get all information about lists not requiring parameters (ie. list_origin_access_identities, list_distributions, list_streaming_distributions) -- cloudfront_facts: - origin_access_identity: true - origin_access_identity_id: my-cloudfront-origin-access-identity-id - -# Get all information about lists not requiring parameters (ie. list_origin_access_identities, list_distributions, list_streaming_distributions) -- cloudfront_facts: - all_lists: true -''' - -RETURN = ''' -origin_access_identity: - description: Describes the origin access identity information. Requires I(origin_access_identity_id) to be set. - returned: only if I(origin_access_identity) is true - type: dict -origin_access_identity_configuration: - description: Describes the origin access identity information configuration information. Requires I(origin_access_identity_id) to be set. - returned: only if I(origin_access_identity_configuration) is true - type: dict -distribution: - description: > - Facts about a CloudFront distribution. Requires I(distribution_id) or I(domain_name_alias) - to be specified. Requires I(origin_access_identity_id) to be set. - returned: only if distribution is true - type: dict -distribution_config: - description: > - Facts about a CloudFront distribution's config. Requires I(distribution_id) or I(domain_name_alias) - to be specified. - returned: only if I(distribution_config) is true - type: dict -invalidation: - description: > - Describes the invalidation information for the distribution. Requires - I(invalidation_id) to be specified and either I(distribution_id) or I(domain_name_alias.) - returned: only if invalidation is true - type: dict -streaming_distribution: - description: > - Describes the streaming information for the distribution. Requires - I(distribution_id) or I(domain_name_alias) to be specified. - returned: only if I(streaming_distribution) is true - type: dict -streaming_distribution_config: - description: > - Describes the streaming configuration information for the distribution. - Requires I(distribution_id) or I(domain_name_alias) to be specified. - returned: only if I(streaming_distribution_config) is true - type: dict -summary: - description: Gives a summary of distributions, streaming distributions and origin access identities. - returned: as default or if summary is true - type: dict -result: - description: > - Result dict not nested under the CloudFront ID to access results of module without the knowledge of that id - as figuring out the DistributionId is usually the reason one uses this module in the first place. - returned: always - type: dict -''' - -from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, boto3_conn, HAS_BOTO3 -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict -from ansible.module_utils.basic import AnsibleModule -from functools import partial -import traceback - -try: - import botocore -except ImportError: - pass # will be caught by imported HAS_BOTO3 - - -class CloudFrontServiceManager: - """Handles CloudFront Services""" - - def __init__(self, module): - self.module = module - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - self.client = boto3_conn(module, conn_type='client', - resource='cloudfront', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoRegionError: - self.module.fail_json(msg="Region must be specified as a parameter, in AWS_DEFAULT_REGION " - "environment variable or in boto configuration file") - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Can't establish connection - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_distribution(self, distribution_id): - try: - func = partial(self.client.get_distribution, Id=distribution_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing distribution - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_distribution_config(self, distribution_id): - try: - func = partial(self.client.get_distribution_config, Id=distribution_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing distribution configuration - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_origin_access_identity(self, origin_access_identity_id): - try: - func = partial(self.client.get_cloud_front_origin_access_identity, Id=origin_access_identity_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing origin access identity - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_origin_access_identity_config(self, origin_access_identity_id): - try: - func = partial(self.client.get_cloud_front_origin_access_identity_config, Id=origin_access_identity_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing origin access identity configuration - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_invalidation(self, distribution_id, invalidation_id): - try: - func = partial(self.client.get_invalidation, DistributionId=distribution_id, Id=invalidation_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing invalidation - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_streaming_distribution(self, distribution_id): - try: - func = partial(self.client.get_streaming_distribution, Id=distribution_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing streaming distribution - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_streaming_distribution_config(self, distribution_id): - try: - func = partial(self.client.get_streaming_distribution_config, Id=distribution_id) - return self.paginated_response(func) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error describing streaming distribution - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def list_origin_access_identities(self): - try: - func = partial(self.client.list_cloud_front_origin_access_identities) - origin_access_identity_list = self.paginated_response(func, 'CloudFrontOriginAccessIdentityList') - if origin_access_identity_list['Quantity'] > 0: - return origin_access_identity_list['Items'] - return {} - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error listing cloud front origin access identities - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def list_distributions(self, keyed=True): - try: - func = partial(self.client.list_distributions) - distribution_list = self.paginated_response(func, 'DistributionList') - if distribution_list['Quantity'] == 0: - return {} - else: - distribution_list = distribution_list['Items'] - if not keyed: - return distribution_list - return self.keyed_list_helper(distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error listing distributions - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def list_distributions_by_web_acl_id(self, web_acl_id): - try: - func = partial(self.client.list_distributions_by_web_acl_id, WebAclId=web_acl_id) - distribution_list = self.paginated_response(func, 'DistributionList') - if distribution_list['Quantity'] == 0: - return {} - else: - distribution_list = distribution_list['Items'] - return self.keyed_list_helper(distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error listing distributions by web acl id - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def list_invalidations(self, distribution_id): - try: - func = partial(self.client.list_invalidations, DistributionId=distribution_id) - invalidation_list = self.paginated_response(func, 'InvalidationList') - if invalidation_list['Quantity'] > 0: - return invalidation_list['Items'] - return {} - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error listing invalidations - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def list_streaming_distributions(self, keyed=True): - try: - func = partial(self.client.list_streaming_distributions) - streaming_distribution_list = self.paginated_response(func, 'StreamingDistributionList') - if streaming_distribution_list['Quantity'] == 0: - return {} - else: - streaming_distribution_list = streaming_distribution_list['Items'] - if not keyed: - return streaming_distribution_list - return self.keyed_list_helper(streaming_distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error listing streaming distributions - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def summary(self): - summary_dict = {} - summary_dict.update(self.summary_get_distribution_list(False)) - summary_dict.update(self.summary_get_distribution_list(True)) - summary_dict.update(self.summary_get_origin_access_identity_list()) - return summary_dict - - def summary_get_origin_access_identity_list(self): - try: - origin_access_identity_list = {'origin_access_identities': []} - origin_access_identities = self.list_origin_access_identities() - for origin_access_identity in origin_access_identities: - oai_id = origin_access_identity['Id'] - oai_full_response = self.get_origin_access_identity(oai_id) - oai_summary = {'Id': oai_id, 'ETag': oai_full_response['ETag']} - origin_access_identity_list['origin_access_identities'].append(oai_summary) - return origin_access_identity_list - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error generating summary of origin access identities - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def summary_get_distribution_list(self, streaming=False): - try: - list_name = 'streaming_distributions' if streaming else 'distributions' - key_list = ['Id', 'ARN', 'Status', 'LastModifiedTime', 'DomainName', 'Comment', 'PriceClass', 'Enabled'] - distribution_list = {list_name: []} - distributions = self.list_streaming_distributions(False) if streaming else self.list_distributions(False) - for dist in distributions: - temp_distribution = {} - for key_name in key_list: - temp_distribution[key_name] = dist[key_name] - temp_distribution['Aliases'] = [alias for alias in dist['Aliases'].get('Items', [])] - temp_distribution['ETag'] = self.get_etag_from_distribution_id(dist['Id'], streaming) - if not streaming: - temp_distribution['WebACLId'] = dist['WebACLId'] - invalidation_ids = self.get_list_of_invalidation_ids_from_distribution_id(dist['Id']) - if invalidation_ids: - temp_distribution['Invalidations'] = invalidation_ids - resource_tags = self.client.list_tags_for_resource(Resource=dist['ARN']) - temp_distribution['Tags'] = boto3_tag_list_to_ansible_dict(resource_tags['Tags'].get('Items', [])) - distribution_list[list_name].append(temp_distribution) - return distribution_list - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error generating summary of distributions - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except Exception as e: - self.module.fail_json(msg="Error generating summary of distributions - " + str(e), - exception=traceback.format_exc()) - - def get_etag_from_distribution_id(self, distribution_id, streaming): - distribution = {} - if not streaming: - distribution = self.get_distribution(distribution_id) - else: - distribution = self.get_streaming_distribution(distribution_id) - return distribution['ETag'] - - def get_list_of_invalidation_ids_from_distribution_id(self, distribution_id): - try: - invalidation_ids = [] - invalidations = self.list_invalidations(distribution_id) - for invalidation in invalidations: - invalidation_ids.append(invalidation['Id']) - return invalidation_ids - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error getting list of invalidation ids - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_distribution_id_from_domain_name(self, domain_name): - try: - distribution_id = "" - distributions = self.list_distributions(False) - distributions += self.list_streaming_distributions(False) - for dist in distributions: - if 'Items' in dist['Aliases']: - for alias in dist['Aliases']['Items']: - if str(alias).lower() == domain_name.lower(): - distribution_id = dist['Id'] - break - return distribution_id - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error getting distribution id from domain name - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def get_aliases_from_distribution_id(self, distribution_id): - aliases = [] - try: - distributions = self.list_distributions(False) - for dist in distributions: - if dist['Id'] == distribution_id and 'Items' in dist['Aliases']: - for alias in dist['Aliases']['Items']: - aliases.append(alias) - break - return aliases - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg="Error getting list of aliases from distribution_id - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - def paginated_response(self, func, result_key=""): - ''' - Returns expanded response for paginated operations. - The 'result_key' is used to define the concatenated results that are combined from each paginated response. - ''' - args = dict() - results = dict() - loop = True - while loop: - response = func(**args) - if result_key == "": - result = response - result.pop('ResponseMetadata', None) - else: - result = response.get(result_key) - results.update(result) - args['Marker'] = response.get('NextMarker') - for key in response.keys(): - if key.endswith('List'): - args['Marker'] = response[key].get('NextMarker') - break - loop = args['Marker'] is not None - return results - - def keyed_list_helper(self, list_to_key): - keyed_list = dict() - for item in list_to_key: - distribution_id = item['Id'] - if 'Items' in item['Aliases']: - aliases = item['Aliases']['Items'] - for alias in aliases: - keyed_list.update({alias: item}) - keyed_list.update({distribution_id: item}) - return keyed_list - - -def set_facts_for_distribution_id_and_alias(details, facts, distribution_id, aliases): - facts[distribution_id].update(details) - # also have a fixed key for accessing results/details returned - facts['result'] = details - facts['result']['DistributionId'] = distribution_id - - for alias in aliases: - facts[alias].update(details) - return facts - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - distribution_id=dict(required=False, type='str'), - invalidation_id=dict(required=False, type='str'), - origin_access_identity_id=dict(required=False, type='str'), - domain_name_alias=dict(required=False, type='str'), - all_lists=dict(required=False, default=False, type='bool'), - distribution=dict(required=False, default=False, type='bool'), - distribution_config=dict(required=False, default=False, type='bool'), - origin_access_identity=dict(required=False, default=False, type='bool'), - origin_access_identity_config=dict(required=False, default=False, type='bool'), - invalidation=dict(required=False, default=False, type='bool'), - streaming_distribution=dict(required=False, default=False, type='bool'), - streaming_distribution_config=dict(required=False, default=False, type='bool'), - list_origin_access_identities=dict(required=False, default=False, type='bool'), - list_distributions=dict(required=False, default=False, type='bool'), - list_distributions_by_web_acl_id=dict(required=False, default=False, type='bool'), - list_invalidations=dict(required=False, default=False, type='bool'), - list_streaming_distributions=dict(required=False, default=False, type='bool'), - summary=dict(required=False, default=False, type='bool') - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) - is_old_facts = module._name == 'cloudfront_facts' - if is_old_facts: - module.deprecate("The 'cloudfront_facts' module has been renamed to 'cloudfront_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - service_mgr = CloudFrontServiceManager(module) - - distribution_id = module.params.get('distribution_id') - invalidation_id = module.params.get('invalidation_id') - origin_access_identity_id = module.params.get('origin_access_identity_id') - web_acl_id = module.params.get('web_acl_id') - domain_name_alias = module.params.get('domain_name_alias') - all_lists = module.params.get('all_lists') - distribution = module.params.get('distribution') - distribution_config = module.params.get('distribution_config') - origin_access_identity = module.params.get('origin_access_identity') - origin_access_identity_config = module.params.get('origin_access_identity_config') - invalidation = module.params.get('invalidation') - streaming_distribution = module.params.get('streaming_distribution') - streaming_distribution_config = module.params.get('streaming_distribution_config') - list_origin_access_identities = module.params.get('list_origin_access_identities') - list_distributions = module.params.get('list_distributions') - list_distributions_by_web_acl_id = module.params.get('list_distributions_by_web_acl_id') - list_invalidations = module.params.get('list_invalidations') - list_streaming_distributions = module.params.get('list_streaming_distributions') - summary = module.params.get('summary') - - aliases = [] - result = {'cloudfront': {}} - facts = {} - - require_distribution_id = (distribution or distribution_config or invalidation or streaming_distribution or - streaming_distribution_config or list_invalidations) - - # set default to summary if no option specified - summary = summary or not (distribution or distribution_config or origin_access_identity or - origin_access_identity_config or invalidation or streaming_distribution or streaming_distribution_config or - list_origin_access_identities or list_distributions_by_web_acl_id or list_invalidations or - list_streaming_distributions or list_distributions) - - # validations - if require_distribution_id and distribution_id is None and domain_name_alias is None: - module.fail_json(msg='Error distribution_id or domain_name_alias have not been specified.') - if (invalidation and invalidation_id is None): - module.fail_json(msg='Error invalidation_id has not been specified.') - if (origin_access_identity or origin_access_identity_config) and origin_access_identity_id is None: - module.fail_json(msg='Error origin_access_identity_id has not been specified.') - if list_distributions_by_web_acl_id and web_acl_id is None: - module.fail_json(msg='Error web_acl_id has not been specified.') - - # get distribution id from domain name alias - if require_distribution_id and distribution_id is None: - distribution_id = service_mgr.get_distribution_id_from_domain_name(domain_name_alias) - if not distribution_id: - module.fail_json(msg='Error unable to source a distribution id from domain_name_alias') - - # set appropriate cloudfront id - if distribution_id and not list_invalidations: - facts = {distribution_id: {}} - aliases = service_mgr.get_aliases_from_distribution_id(distribution_id) - for alias in aliases: - facts.update({alias: {}}) - if invalidation_id: - facts.update({invalidation_id: {}}) - elif distribution_id and list_invalidations: - facts = {distribution_id: {}} - aliases = service_mgr.get_aliases_from_distribution_id(distribution_id) - for alias in aliases: - facts.update({alias: {}}) - elif origin_access_identity_id: - facts = {origin_access_identity_id: {}} - elif web_acl_id: - facts = {web_acl_id: {}} - - # get details based on options - if distribution: - facts_to_set = service_mgr.get_distribution(distribution_id) - if distribution_config: - facts_to_set = service_mgr.get_distribution_config(distribution_id) - if origin_access_identity: - facts[origin_access_identity_id].update(service_mgr.get_origin_access_identity(origin_access_identity_id)) - if origin_access_identity_config: - facts[origin_access_identity_id].update(service_mgr.get_origin_access_identity_config(origin_access_identity_id)) - if invalidation: - facts_to_set = service_mgr.get_invalidation(distribution_id, invalidation_id) - facts[invalidation_id].update(facts_to_set) - if streaming_distribution: - facts_to_set = service_mgr.get_streaming_distribution(distribution_id) - if streaming_distribution_config: - facts_to_set = service_mgr.get_streaming_distribution_config(distribution_id) - if list_invalidations: - facts_to_set = {'invalidations': service_mgr.list_invalidations(distribution_id)} - if 'facts_to_set' in vars(): - facts = set_facts_for_distribution_id_and_alias(facts_to_set, facts, distribution_id, aliases) - - # get list based on options - if all_lists or list_origin_access_identities: - facts['origin_access_identities'] = service_mgr.list_origin_access_identities() - if all_lists or list_distributions: - facts['distributions'] = service_mgr.list_distributions() - if all_lists or list_streaming_distributions: - facts['streaming_distributions'] = service_mgr.list_streaming_distributions() - if list_distributions_by_web_acl_id: - facts['distributions_by_web_acl_id'] = service_mgr.list_distributions_by_web_acl_id(web_acl_id) - if list_invalidations: - facts['invalidations'] = service_mgr.list_invalidations(distribution_id) - - # default summary option - if summary: - facts['summary'] = service_mgr.summary() - - result['changed'] = False - result['cloudfront'].update(facts) - if is_old_facts: - module.exit_json(msg="Retrieved CloudFront facts.", ansible_facts=result) - else: - module.exit_json(msg="Retrieved CloudFront info.", **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_invalidation.py b/lib/ansible/modules/cloud/amazon/cloudfront_invalidation.py deleted file mode 100644 index 10265a0950..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudfront_invalidation.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- - -module: cloudfront_invalidation - -short_description: create invalidations for AWS CloudFront distributions -description: - - Allows for invalidation of a batch of paths for a CloudFront distribution. - -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 - -version_added: "2.5" - -author: Willem van Ketwich (@wilvk) - -extends_documentation_fragment: - - aws - - ec2 - -options: - distribution_id: - description: - - The ID of the CloudFront distribution to invalidate paths for. Can be specified instead of the alias. - required: false - type: str - alias: - description: - - The alias of the CloudFront distribution to invalidate paths for. Can be specified instead of distribution_id. - required: false - type: str - caller_reference: - description: - - A unique reference identifier for the invalidation paths. - - Defaults to current datetime stamp. - required: false - default: - type: str - target_paths: - description: - - A list of paths on the distribution to invalidate. Each path should begin with '/'. Wildcards are allowed. eg. '/foo/bar/*' - required: true - type: list - elements: str - -notes: - - does not support check mode - -''' - -EXAMPLES = ''' - -- name: create a batch of invalidations using a distribution_id for a reference - cloudfront_invalidation: - distribution_id: E15BU8SDCGSG57 - caller_reference: testing 123 - target_paths: - - /testpathone/test1.css - - /testpathtwo/test2.js - - /testpaththree/test3.ss - -- name: create a batch of invalidations using an alias as a reference and one path using a wildcard match - cloudfront_invalidation: - alias: alias.test.com - caller_reference: testing 123 - target_paths: - - /testpathone/test4.css - - /testpathtwo/test5.js - - /testpaththree/* - -''' - -RETURN = ''' -invalidation: - description: The invalidation's information. - returned: always - type: complex - contains: - create_time: - description: The date and time the invalidation request was first made. - returned: always - type: str - sample: '2018-02-01T15:50:41.159000+00:00' - id: - description: The identifier for the invalidation request. - returned: always - type: str - sample: I2G9MOWJZFV612 - invalidation_batch: - description: The current invalidation information for the batch request. - returned: always - type: complex - contains: - caller_reference: - description: The value used to uniquely identify an invalidation request. - returned: always - type: str - sample: testing 123 - paths: - description: A dict that contains information about the objects that you want to invalidate. - returned: always - type: complex - contains: - items: - description: A list of the paths that you want to invalidate. - returned: always - type: list - sample: - - /testpathtwo/test2.js - - /testpathone/test1.css - - /testpaththree/test3.ss - quantity: - description: The number of objects that you want to invalidate. - returned: always - type: int - sample: 3 - status: - description: The status of the invalidation request. - returned: always - type: str - sample: Completed -location: - description: The fully qualified URI of the distribution and invalidation batch request. - returned: always - type: str - sample: https://cloudfront.amazonaws.com/2017-03-25/distribution/E1ZID6KZJECZY7/invalidation/I2G9MOWJZFV622 -''' - -from ansible.module_utils.ec2 import snake_dict_to_camel_dict -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager -import datetime - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by imported AnsibleAWSModule - - -class CloudFrontInvalidationServiceManager(object): - """ - Handles CloudFront service calls to AWS for invalidations - """ - - def __init__(self, module): - self.module = module - self.client = module.client('cloudfront') - - def create_invalidation(self, distribution_id, invalidation_batch): - current_invalidation_response = self.get_invalidation(distribution_id, invalidation_batch['CallerReference']) - try: - response = self.client.create_invalidation(DistributionId=distribution_id, InvalidationBatch=invalidation_batch) - response.pop('ResponseMetadata', None) - if current_invalidation_response: - return response, False - else: - return response, True - except BotoCoreError as e: - self.module.fail_json_aws(e, msg="Error creating CloudFront invalidations.") - except ClientError as e: - if ('Your request contains a caller reference that was used for a previous invalidation batch ' - 'for the same distribution.' in e.response['Error']['Message']): - self.module.warn("InvalidationBatch target paths are not modifiable. " - "To make a new invalidation please update caller_reference.") - return current_invalidation_response, False - else: - self.module.fail_json_aws(e, msg="Error creating CloudFront invalidations.") - - def get_invalidation(self, distribution_id, caller_reference): - current_invalidation = {} - # find all invalidations for the distribution - try: - paginator = self.client.get_paginator('list_invalidations') - invalidations = paginator.paginate(DistributionId=distribution_id).build_full_result().get('InvalidationList', {}).get('Items', []) - invalidation_ids = [inv['Id'] for inv in invalidations] - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Error listing CloudFront invalidations.") - - # check if there is an invalidation with the same caller reference - for inv_id in invalidation_ids: - try: - invalidation = self.client.get_invalidation(DistributionId=distribution_id, Id=inv_id)['Invalidation'] - caller_ref = invalidation.get('InvalidationBatch', {}).get('CallerReference') - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, msg="Error getting CloudFront invalidation {0}".format(inv_id)) - if caller_ref == caller_reference: - current_invalidation = invalidation - break - - current_invalidation.pop('ResponseMetadata', None) - return current_invalidation - - -class CloudFrontInvalidationValidationManager(object): - """ - Manages CloudFront validations for invalidation batches - """ - - def __init__(self, module): - self.module = module - self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module) - - def validate_distribution_id(self, distribution_id, alias): - try: - if distribution_id is None and alias is None: - self.module.fail_json(msg="distribution_id or alias must be specified") - if distribution_id is None: - distribution_id = self.__cloudfront_facts_mgr.get_distribution_id_from_domain_name(alias) - return distribution_id - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error validating parameters.") - - def create_aws_list(self, invalidation_batch): - aws_list = {} - aws_list["Quantity"] = len(invalidation_batch) - aws_list["Items"] = invalidation_batch - return aws_list - - def validate_invalidation_batch(self, invalidation_batch, caller_reference): - try: - if caller_reference is not None: - valid_caller_reference = caller_reference - else: - valid_caller_reference = datetime.datetime.now().isoformat() - valid_invalidation_batch = { - 'paths': self.create_aws_list(invalidation_batch), - 'caller_reference': valid_caller_reference - } - return valid_invalidation_batch - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error validating invalidation batch.") - - -def main(): - argument_spec = dict( - caller_reference=dict(), - distribution_id=dict(), - alias=dict(), - target_paths=dict(required=True, type='list') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=False, mutually_exclusive=[['distribution_id', 'alias']]) - - validation_mgr = CloudFrontInvalidationValidationManager(module) - service_mgr = CloudFrontInvalidationServiceManager(module) - - caller_reference = module.params.get('caller_reference') - distribution_id = module.params.get('distribution_id') - alias = module.params.get('alias') - target_paths = module.params.get('target_paths') - - result = {} - - distribution_id = validation_mgr.validate_distribution_id(distribution_id, alias) - valid_target_paths = validation_mgr.validate_invalidation_batch(target_paths, caller_reference) - valid_pascal_target_paths = snake_dict_to_camel_dict(valid_target_paths, True) - result, changed = service_mgr.create_invalidation(distribution_id, valid_pascal_target_paths) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(result)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_origin_access_identity.py b/lib/ansible/modules/cloud/amazon/cloudfront_origin_access_identity.py deleted file mode 100644 index 44381fcbfa..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudfront_origin_access_identity.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- - -module: cloudfront_origin_access_identity - -short_description: Create, update and delete origin access identities for a - CloudFront distribution - -description: - - Allows for easy creation, updating and deletion of origin access - identities. - -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 - -version_added: "2.5" - -author: Willem van Ketwich (@wilvk) - -extends_documentation_fragment: - - aws - - ec2 - -options: - state: - description: If the named resource should exist. - choices: - - present - - absent - default: present - type: str - origin_access_identity_id: - description: - - The origin_access_identity_id of the CloudFront distribution. - required: false - type: str - comment: - description: - - A comment to describe the CloudFront origin access identity. - required: false - type: str - caller_reference: - description: - - A unique identifier to reference the origin access identity by. - required: false - type: str - -notes: - - Does not support check mode. - -''' - -EXAMPLES = ''' - -- name: create an origin access identity - cloudfront_origin_access_identity: - state: present - caller_reference: this is an example reference - comment: this is an example comment - -- name: update an existing origin access identity using caller_reference as an identifier - cloudfront_origin_access_identity: - origin_access_identity_id: E17DRN9XUOAHZX - caller_reference: this is an example reference - comment: this is a new comment - -- name: delete an existing origin access identity using caller_reference as an identifier - cloudfront_origin_access_identity: - state: absent - caller_reference: this is an example reference - comment: this is a new comment - -''' - -RETURN = ''' -cloud_front_origin_access_identity: - description: The origin access identity's information. - returned: always - type: complex - contains: - cloud_front_origin_access_identity_config: - description: describes a url specifying the origin access identity. - returned: always - type: complex - contains: - caller_reference: - description: a caller reference for the oai - returned: always - type: str - comment: - description: a comment describing the oai - returned: always - type: str - id: - description: a unique identifier of the oai - returned: always - type: str - s3_canonical_user_id: - description: the canonical user ID of the user who created the oai - returned: always - type: str -e_tag: - description: The current version of the origin access identity created. - returned: always - type: str -location: - description: The fully qualified URI of the new origin access identity just created. - returned: when initially created - type: str - -''' - -from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils.aws.core import AnsibleAWSModule -import datetime -from functools import partial -import json -import traceback - -try: - import botocore - from botocore.signers import CloudFrontSigner - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by imported AnsibleAWSModule - - -class CloudFrontOriginAccessIdentityServiceManager(object): - """ - Handles CloudFront origin access identity service calls to aws - """ - - def __init__(self, module): - self.module = module - self.client = module.client('cloudfront') - - def create_origin_access_identity(self, caller_reference, comment): - try: - return self.client.create_cloud_front_origin_access_identity( - CloudFrontOriginAccessIdentityConfig={ - 'CallerReference': caller_reference, - 'Comment': comment - } - ) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error creating cloud front origin access identity.") - - def delete_origin_access_identity(self, origin_access_identity_id, e_tag): - try: - return self.client.delete_cloud_front_origin_access_identity(Id=origin_access_identity_id, IfMatch=e_tag) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error updating Origin Access Identity.") - - def update_origin_access_identity(self, caller_reference, comment, origin_access_identity_id, e_tag): - changed = False - new_config = { - 'CallerReference': caller_reference, - 'Comment': comment - } - - try: - current_config = self.client.get_cloud_front_origin_access_identity_config( - Id=origin_access_identity_id)['CloudFrontOriginAccessIdentityConfig'] - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error getting Origin Access Identity config.") - - if new_config != current_config: - changed = True - - try: - # If the CallerReference is a value already sent in a previous identity request - # the returned value is that of the original request - result = self.client.update_cloud_front_origin_access_identity( - CloudFrontOriginAccessIdentityConfig=new_config, - Id=origin_access_identity_id, - IfMatch=e_tag, - ) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error updating Origin Access Identity.") - - return result, changed - - -class CloudFrontOriginAccessIdentityValidationManager(object): - """ - Manages CloudFront Origin Access Identities - """ - - def __init__(self, module): - self.module = module - self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module) - - def validate_etag_from_origin_access_identity_id(self, origin_access_identity_id): - try: - if origin_access_identity_id is None: - return - oai = self.__cloudfront_facts_mgr.get_origin_access_identity(origin_access_identity_id) - if oai is not None: - return oai.get('ETag') - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error getting etag from origin_access_identity.") - - def validate_origin_access_identity_id_from_caller_reference( - self, caller_reference): - try: - origin_access_identities = self.__cloudfront_facts_mgr.list_origin_access_identities() - origin_origin_access_identity_ids = [oai.get('Id') for oai in origin_access_identities] - for origin_access_identity_id in origin_origin_access_identity_ids: - oai_config = (self.__cloudfront_facts_mgr.get_origin_access_identity_config(origin_access_identity_id)) - temp_caller_reference = oai_config.get('CloudFrontOriginAccessIdentityConfig').get('CallerReference') - if temp_caller_reference == caller_reference: - return origin_access_identity_id - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Error getting Origin Access Identity from caller_reference.") - - def validate_comment(self, comment): - if comment is None: - return "origin access identity created by Ansible with datetime " + datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') - return comment - - -def main(): - argument_spec = dict( - state=dict(choices=['present', 'absent'], default='present'), - origin_access_identity_id=dict(), - caller_reference=dict(), - comment=dict(), - ) - - result = {} - e_tag = None - changed = False - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=False) - service_mgr = CloudFrontOriginAccessIdentityServiceManager(module) - validation_mgr = CloudFrontOriginAccessIdentityValidationManager(module) - - state = module.params.get('state') - caller_reference = module.params.get('caller_reference') - - comment = module.params.get('comment') - origin_access_identity_id = module.params.get('origin_access_identity_id') - - if origin_access_identity_id is None and caller_reference is not None: - origin_access_identity_id = validation_mgr.validate_origin_access_identity_id_from_caller_reference(caller_reference) - - e_tag = validation_mgr.validate_etag_from_origin_access_identity_id(origin_access_identity_id) - comment = validation_mgr.validate_comment(comment) - - if state == 'present': - if origin_access_identity_id is not None and e_tag is not None: - result, changed = service_mgr.update_origin_access_identity(caller_reference, comment, origin_access_identity_id, e_tag) - else: - result = service_mgr.create_origin_access_identity(caller_reference, comment) - changed = True - elif(state == 'absent' and origin_access_identity_id is not None and - e_tag is not None): - result = service_mgr.delete_origin_access_identity(origin_access_identity_id, e_tag) - changed = True - - result.pop('ResponseMetadata', None) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(result)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudtrail.py b/lib/ansible/modules/cloud/amazon/cloudtrail.py deleted file mode 100644 index 382e8b15f0..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudtrail.py +++ /dev/null @@ -1,618 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudtrail -short_description: manage CloudTrail create, delete, update -description: - - Creates, deletes, or updates CloudTrail configuration. Ensures logging is also enabled. -version_added: "2.0" -author: - - Ansible Core Team - - Ted Timmons (@tedder) - - Daniel Shepherd (@shepdelacreme) -requirements: - - boto3 - - botocore -options: - state: - description: - - Add or remove CloudTrail configuration. - - 'The following states have been preserved for backwards compatibility: I(state=enabled) and I(state=disabled).' - - I(state=enabled) is equivalet to I(state=present). - - I(state=disabled) is equivalet to I(state=absent). - type: str - choices: ['present', 'absent', 'enabled', 'disabled'] - default: present - name: - description: - - Name for the CloudTrail. - - Names are unique per-region unless the CloudTrail is a multi-region trail, in which case it is unique per-account. - type: str - default: default - enable_logging: - description: - - Start or stop the CloudTrail logging. If stopped the trail will be paused and will not record events or deliver log files. - default: true - type: bool - version_added: "2.4" - s3_bucket_name: - description: - - An existing S3 bucket where CloudTrail will deliver log files. - - This bucket should exist and have the proper policy. - - See U(https://docs.aws.amazon.com/awscloudtrail/latest/userguide/aggregating_logs_regions_bucket_policy.html). - - Required when I(state=present). - type: str - version_added: "2.4" - s3_key_prefix: - description: - - S3 Key prefix for delivered log files. A trailing slash is not necessary and will be removed. - type: str - is_multi_region_trail: - description: - - Specify whether the trail belongs only to one region or exists in all regions. - default: false - type: bool - version_added: "2.4" - enable_log_file_validation: - description: - - Specifies whether log file integrity validation is enabled. - - CloudTrail will create a hash for every log file delivered and produce a signed digest file that can be used to ensure log files have not been tampered. - version_added: "2.4" - type: bool - aliases: [ "log_file_validation_enabled" ] - include_global_events: - description: - - Record API calls from global services such as IAM and STS. - default: true - type: bool - aliases: [ "include_global_service_events" ] - sns_topic_name: - description: - - SNS Topic name to send notifications to when a log file is delivered. - version_added: "2.4" - type: str - cloudwatch_logs_role_arn: - description: - - Specifies a full ARN for an IAM role that assigns the proper permissions for CloudTrail to create and write to the log group. - - See U(https://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html). - - Required when C(cloudwatch_logs_log_group_arn). - version_added: "2.4" - type: str - cloudwatch_logs_log_group_arn: - description: - - A full ARN specifying a valid CloudWatch log group to which CloudTrail logs will be delivered. The log group should already exist. - - See U(https://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html). - - Required when C(cloudwatch_logs_role_arn). - type: str - version_added: "2.4" - kms_key_id: - description: - - Specifies the KMS key ID to use to encrypt the logs delivered by CloudTrail. This also has the effect of enabling log file encryption. - - The value can be an alias name prefixed by "alias/", a fully specified ARN to an alias, a fully specified ARN to a key, or a globally unique identifier. - - See U(https://docs.aws.amazon.com/awscloudtrail/latest/userguide/encrypting-cloudtrail-log-files-with-aws-kms.html). - type: str - version_added: "2.4" - tags: - description: - - A hash/dictionary of tags to be applied to the CloudTrail resource. - - Remove completely or specify an empty dictionary to remove all tags. - default: {} - version_added: "2.4" - type: dict - -extends_documentation_fragment: -- aws -- ec2 -''' - -EXAMPLES = ''' -- name: create single region cloudtrail - cloudtrail: - state: present - name: default - s3_bucket_name: mylogbucket - s3_key_prefix: cloudtrail - region: us-east-1 - -- name: create multi-region trail with validation and tags - cloudtrail: - state: present - name: default - s3_bucket_name: mylogbucket - region: us-east-1 - is_multi_region_trail: true - enable_log_file_validation: true - cloudwatch_logs_role_arn: "arn:aws:iam::123456789012:role/CloudTrail_CloudWatchLogs_Role" - cloudwatch_logs_log_group_arn: "arn:aws:logs:us-east-1:123456789012:log-group:CloudTrail/DefaultLogGroup:*" - kms_key_id: "alias/MyAliasName" - tags: - environment: dev - Name: default - -- name: show another valid kms_key_id - cloudtrail: - state: present - name: default - s3_bucket_name: mylogbucket - kms_key_id: "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" - # simply "12345678-1234-1234-1234-123456789012" would be valid too. - -- name: pause logging the trail we just created - cloudtrail: - state: present - name: default - enable_logging: false - s3_bucket_name: mylogbucket - region: us-east-1 - is_multi_region_trail: true - enable_log_file_validation: true - tags: - environment: dev - Name: default - -- name: delete a trail - cloudtrail: - state: absent - name: default -''' - -RETURN = ''' -exists: - description: whether the resource exists - returned: always - type: bool - sample: true -trail: - description: CloudTrail resource details - returned: always - type: complex - sample: hash/dictionary of values - contains: - trail_arn: - description: Full ARN of the CloudTrail resource - returned: success - type: str - sample: arn:aws:cloudtrail:us-east-1:123456789012:trail/default - name: - description: Name of the CloudTrail resource - returned: success - type: str - sample: default - is_logging: - description: Whether logging is turned on or paused for the Trail - returned: success - type: bool - sample: True - s3_bucket_name: - description: S3 bucket name where log files are delivered - returned: success - type: str - sample: myBucket - s3_key_prefix: - description: Key prefix in bucket where log files are delivered (if any) - returned: success when present - type: str - sample: myKeyPrefix - log_file_validation_enabled: - description: Whether log file validation is enabled on the trail - returned: success - type: bool - sample: true - include_global_service_events: - description: Whether global services (IAM, STS) are logged with this trail - returned: success - type: bool - sample: true - is_multi_region_trail: - description: Whether the trail applies to all regions or just one - returned: success - type: bool - sample: true - has_custom_event_selectors: - description: Whether any custom event selectors are used for this trail. - returned: success - type: bool - sample: False - home_region: - description: The home region where the trail was originally created and must be edited. - returned: success - type: str - sample: us-east-1 - sns_topic_name: - description: The SNS topic name where log delivery notifications are sent. - returned: success when present - type: str - sample: myTopic - sns_topic_arn: - description: Full ARN of the SNS topic where log delivery notifications are sent. - returned: success when present - type: str - sample: arn:aws:sns:us-east-1:123456789012:topic/myTopic - cloud_watch_logs_log_group_arn: - description: Full ARN of the CloudWatch Logs log group where events are delivered. - returned: success when present - type: str - sample: arn:aws:logs:us-east-1:123456789012:log-group:CloudTrail/DefaultLogGroup:* - cloud_watch_logs_role_arn: - description: Full ARN of the IAM role that CloudTrail assumes to deliver events. - returned: success when present - type: str - sample: arn:aws:iam::123456789012:role/CloudTrail_CloudWatchLogs_Role - kms_key_id: - description: Full ARN of the KMS Key used to encrypt log files. - returned: success when present - type: str - sample: arn:aws:kms::123456789012:key/12345678-1234-1234-1234-123456789012 - tags: - description: hash/dictionary of tags applied to this resource - returned: success - type: dict - sample: {'environment': 'dev', 'Name': 'default'} -''' - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, - ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict) - - -def create_trail(module, client, ct_params): - """ - Creates a CloudTrail - - module : AnsibleAWSModule object - client : boto3 client connection object - ct_params : The parameters for the Trail to create - """ - resp = {} - try: - resp = client.create_trail(**ct_params) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to create Trail") - - return resp - - -def tag_trail(module, client, tags, trail_arn, curr_tags=None, dry_run=False): - """ - Creates, updates, removes tags on a CloudTrail resource - - module : AnsibleAWSModule object - client : boto3 client connection object - tags : Dict of tags converted from ansible_dict to boto3 list of dicts - trail_arn : The ARN of the CloudTrail to operate on - curr_tags : Dict of the current tags on resource, if any - dry_run : true/false to determine if changes will be made if needed - """ - adds = [] - removes = [] - updates = [] - changed = False - - if curr_tags is None: - # No current tags so just convert all to a tag list - adds = ansible_dict_to_boto3_tag_list(tags) - else: - curr_keys = set(curr_tags.keys()) - new_keys = set(tags.keys()) - add_keys = new_keys - curr_keys - remove_keys = curr_keys - new_keys - update_keys = dict() - for k in curr_keys.intersection(new_keys): - if curr_tags[k] != tags[k]: - update_keys.update({k: tags[k]}) - - adds = get_tag_list(add_keys, tags) - removes = get_tag_list(remove_keys, curr_tags) - updates = get_tag_list(update_keys, tags) - - if removes or updates: - changed = True - if not dry_run: - try: - client.remove_tags(ResourceId=trail_arn, TagsList=removes + updates) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to remove tags from Trail") - - if updates or adds: - changed = True - if not dry_run: - try: - client.add_tags(ResourceId=trail_arn, TagsList=updates + adds) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to add tags to Trail") - - return changed - - -def get_tag_list(keys, tags): - """ - Returns a list of dicts with tags to act on - keys : set of keys to get the values for - tags : the dict of tags to turn into a list - """ - tag_list = [] - for k in keys: - tag_list.append({'Key': k, 'Value': tags[k]}) - - return tag_list - - -def set_logging(module, client, name, action): - """ - Starts or stops logging based on given state - - module : AnsibleAWSModule object - client : boto3 client connection object - name : The name or ARN of the CloudTrail to operate on - action : start or stop - """ - if action == 'start': - try: - client.start_logging(Name=name) - return client.get_trail_status(Name=name) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to start logging") - elif action == 'stop': - try: - client.stop_logging(Name=name) - return client.get_trail_status(Name=name) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to stop logging") - else: - module.fail_json(msg="Unsupported logging action") - - -def get_trail_facts(module, client, name): - """ - Describes existing trail in an account - - module : AnsibleAWSModule object - client : boto3 client connection object - name : Name of the trail - """ - # get Trail info - try: - trail_resp = client.describe_trails(trailNameList=[name]) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to describe Trail") - - # Now check to see if our trail exists and get status and tags - if len(trail_resp['trailList']): - trail = trail_resp['trailList'][0] - try: - status_resp = client.get_trail_status(Name=trail['Name']) - tags_list = client.list_tags(ResourceIdList=[trail['TrailARN']]) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to describe Trail") - - trail['IsLogging'] = status_resp['IsLogging'] - trail['tags'] = boto3_tag_list_to_ansible_dict(tags_list['ResourceTagList'][0]['TagsList']) - # Check for non-existent values and populate with None - optional_vals = set(['S3KeyPrefix', 'SnsTopicName', 'SnsTopicARN', 'CloudWatchLogsLogGroupArn', 'CloudWatchLogsRoleArn', 'KmsKeyId']) - for v in optional_vals - set(trail.keys()): - trail[v] = None - return trail - - else: - # trail doesn't exist return None - return None - - -def delete_trail(module, client, trail_arn): - """ - Delete a CloudTrail - - module : AnsibleAWSModule object - client : boto3 client connection object - trail_arn : Full CloudTrail ARN - """ - try: - client.delete_trail(Name=trail_arn) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to delete Trail") - - -def update_trail(module, client, ct_params): - """ - Delete a CloudTrail - - module : AnsibleAWSModule object - client : boto3 client connection object - ct_params : The parameters for the Trail to update - """ - try: - client.update_trail(**ct_params) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to update Trail") - - -def main(): - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent', 'enabled', 'disabled']), - name=dict(default='default'), - enable_logging=dict(default=True, type='bool'), - s3_bucket_name=dict(), - s3_key_prefix=dict(), - sns_topic_name=dict(), - is_multi_region_trail=dict(default=False, type='bool'), - enable_log_file_validation=dict(type='bool', aliases=['log_file_validation_enabled']), - include_global_events=dict(default=True, type='bool', aliases=['include_global_service_events']), - cloudwatch_logs_role_arn=dict(), - cloudwatch_logs_log_group_arn=dict(), - kms_key_id=dict(), - tags=dict(default={}, type='dict'), - ) - - required_if = [('state', 'present', ['s3_bucket_name']), ('state', 'enabled', ['s3_bucket_name'])] - required_together = [('cloudwatch_logs_role_arn', 'cloudwatch_logs_log_group_arn')] - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together, required_if=required_if) - - # collect parameters - if module.params['state'] in ('present', 'enabled'): - state = 'present' - elif module.params['state'] in ('absent', 'disabled'): - state = 'absent' - tags = module.params['tags'] - enable_logging = module.params['enable_logging'] - ct_params = dict( - Name=module.params['name'], - S3BucketName=module.params['s3_bucket_name'], - IncludeGlobalServiceEvents=module.params['include_global_events'], - IsMultiRegionTrail=module.params['is_multi_region_trail'], - ) - - if module.params['s3_key_prefix']: - ct_params['S3KeyPrefix'] = module.params['s3_key_prefix'].rstrip('/') - - if module.params['sns_topic_name']: - ct_params['SnsTopicName'] = module.params['sns_topic_name'] - - if module.params['cloudwatch_logs_role_arn']: - ct_params['CloudWatchLogsRoleArn'] = module.params['cloudwatch_logs_role_arn'] - - if module.params['cloudwatch_logs_log_group_arn']: - ct_params['CloudWatchLogsLogGroupArn'] = module.params['cloudwatch_logs_log_group_arn'] - - if module.params['enable_log_file_validation'] is not None: - ct_params['EnableLogFileValidation'] = module.params['enable_log_file_validation'] - - if module.params['kms_key_id']: - ct_params['KmsKeyId'] = module.params['kms_key_id'] - - client = module.client('cloudtrail') - region = module.region - - results = dict( - changed=False, - exists=False - ) - - # Get existing trail facts - trail = get_trail_facts(module, client, ct_params['Name']) - - # If the trail exists set the result exists variable - if trail is not None: - results['exists'] = True - - if state == 'absent' and results['exists']: - # If Trail exists go ahead and delete - results['changed'] = True - results['exists'] = False - results['trail'] = dict() - if not module.check_mode: - delete_trail(module, client, trail['TrailARN']) - - elif state == 'present' and results['exists']: - # If Trail exists see if we need to update it - do_update = False - for key in ct_params: - tkey = str(key) - # boto3 has inconsistent parameter naming so we handle it here - if key == 'EnableLogFileValidation': - tkey = 'LogFileValidationEnabled' - # We need to make an empty string equal None - if ct_params.get(key) == '': - val = None - else: - val = ct_params.get(key) - if val != trail.get(tkey): - do_update = True - results['changed'] = True - # If we are in check mode copy the changed values to the trail facts in result output to show what would change. - if module.check_mode: - trail.update({tkey: ct_params.get(key)}) - - if not module.check_mode and do_update: - update_trail(module, client, ct_params) - trail = get_trail_facts(module, client, ct_params['Name']) - - # Check if we need to start/stop logging - if enable_logging and not trail['IsLogging']: - results['changed'] = True - trail['IsLogging'] = True - if not module.check_mode: - set_logging(module, client, name=ct_params['Name'], action='start') - if not enable_logging and trail['IsLogging']: - results['changed'] = True - trail['IsLogging'] = False - if not module.check_mode: - set_logging(module, client, name=ct_params['Name'], action='stop') - - # Check if we need to update tags on resource - tag_dry_run = False - if module.check_mode: - tag_dry_run = True - tags_changed = tag_trail(module, client, tags=tags, trail_arn=trail['TrailARN'], curr_tags=trail['tags'], dry_run=tag_dry_run) - if tags_changed: - results['changed'] = True - trail['tags'] = tags - # Populate trail facts in output - results['trail'] = camel_dict_to_snake_dict(trail) - - elif state == 'present' and not results['exists']: - # Trail doesn't exist just go create it - results['changed'] = True - if not module.check_mode: - # If we aren't in check_mode then actually create it - created_trail = create_trail(module, client, ct_params) - # Apply tags - tag_trail(module, client, tags=tags, trail_arn=created_trail['TrailARN']) - # Get the trail status - try: - status_resp = client.get_trail_status(Name=created_trail['Name']) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to fetch Trail statuc") - # Set the logging state for the trail to desired value - if enable_logging and not status_resp['IsLogging']: - set_logging(module, client, name=ct_params['Name'], action='start') - if not enable_logging and status_resp['IsLogging']: - set_logging(module, client, name=ct_params['Name'], action='stop') - # Get facts for newly created Trail - trail = get_trail_facts(module, client, ct_params['Name']) - - # If we are in check mode create a fake return structure for the newly minted trail - if module.check_mode: - acct_id = '123456789012' - try: - sts_client = module.client('sts') - acct_id = sts_client.get_caller_identity()['Account'] - except (BotoCoreError, ClientError): - pass - trail = dict() - trail.update(ct_params) - if 'EnableLogFileValidation' not in ct_params: - ct_params['EnableLogFileValidation'] = False - trail['EnableLogFileValidation'] = ct_params['EnableLogFileValidation'] - trail.pop('EnableLogFileValidation') - fake_arn = 'arn:aws:cloudtrail:' + region + ':' + acct_id + ':trail/' + ct_params['Name'] - trail['HasCustomEventSelectors'] = False - trail['HomeRegion'] = region - trail['TrailARN'] = fake_arn - trail['IsLogging'] = enable_logging - trail['tags'] = tags - # Populate trail facts in output - results['trail'] = camel_dict_to_snake_dict(trail) - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudwatchevent_rule.py b/lib/ansible/modules/cloud/amazon/cloudwatchevent_rule.py deleted file mode 100644 index 7939f8c5c8..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudwatchevent_rule.py +++ /dev/null @@ -1,464 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: cloudwatchevent_rule -short_description: Manage CloudWatch Event rules and targets -description: - - This module creates and manages CloudWatch event rules and targets. -version_added: "2.2" -extends_documentation_fragment: - - aws - - ec2 -author: "Jim Dalton (@jsdalton) <jim.dalton@gmail.com>" -requirements: - - python >= 2.6 - - boto3 -notes: - - A rule must contain at least an I(event_pattern) or I(schedule_expression). A - rule can have both an I(event_pattern) and a I(schedule_expression), in which - case the rule will trigger on matching events as well as on a schedule. - - When specifying targets, I(input) and I(input_path) are mutually-exclusive - and optional parameters. -options: - name: - description: - - The name of the rule you are creating, updating or deleting. No spaces - or special characters allowed (i.e. must match C([\.\-_A-Za-z0-9]+)). - required: true - type: str - schedule_expression: - description: - - A cron or rate expression that defines the schedule the rule will - trigger on. For example, C(cron(0 20 * * ? *)), C(rate(5 minutes)). - required: false - type: str - event_pattern: - description: - - A string pattern (in valid JSON format) that is used to match against - incoming events to determine if the rule should be triggered. - required: false - type: str - state: - description: - - Whether the rule is present (and enabled), disabled, or absent. - choices: ["present", "disabled", "absent"] - default: present - required: false - type: str - description: - description: - - A description of the rule. - required: false - type: str - role_arn: - description: - - The Amazon Resource Name (ARN) of the IAM role associated with the rule. - required: false - type: str - targets: - type: list - elements: dict - description: - - A list of targets to add to or update for the rule. - suboptions: - id: - type: str - required: true - description: The unique target assignment ID. - arn: - type: str - required: true - description: The ARN associated with the target. - role_arn: - type: str - description: The ARN of the IAM role to be used for this target when the rule is triggered. - input: - type: str - description: - - A JSON object that will override the event data when passed to the target. - - If neither I(input) nor I(input_path) is specified, then the entire - event is passed to the target in JSON form. - input_path: - type: str - description: - - A JSONPath string (e.g. C($.detail)) that specifies the part of the event data to be - passed to the target. - - If neither I(input) nor I(input_path) is specified, then the entire - event is passed to the target in JSON form. - ecs_parameters: - type: dict - description: - - Contains the ECS task definition and task count to be used, if the event target is an ECS task. - suboptions: - task_definition_arn: - type: str - description: The full ARN of the task definition. - task_count: - type: int - description: The number of tasks to create based on I(task_definition). - required: false -''' - -EXAMPLES = ''' -- cloudwatchevent_rule: - name: MyCronTask - schedule_expression: "cron(0 20 * * ? *)" - description: Run my scheduled task - targets: - - id: MyTargetId - arn: arn:aws:lambda:us-east-1:123456789012:function:MyFunction - -- cloudwatchevent_rule: - name: MyDisabledCronTask - schedule_expression: "rate(5 minutes)" - description: Run my disabled scheduled task - state: disabled - targets: - - id: MyOtherTargetId - arn: arn:aws:lambda:us-east-1:123456789012:function:MyFunction - input: '{"foo": "bar"}' - -- cloudwatchevent_rule: - name: MyCronTask - state: absent -''' - -RETURN = ''' -rule: - description: CloudWatch Event rule data. - returned: success - type: dict - sample: - arn: 'arn:aws:events:us-east-1:123456789012:rule/MyCronTask' - description: 'Run my scheduled task' - name: 'MyCronTask' - schedule_expression: 'cron(0 20 * * ? *)' - state: 'ENABLED' -targets: - description: CloudWatch Event target(s) assigned to the rule. - returned: success - type: list - sample: "[{ 'arn': 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction', 'id': 'MyTargetId' }]" -''' - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -class CloudWatchEventRule(object): - def __init__(self, module, name, client, schedule_expression=None, - event_pattern=None, description=None, role_arn=None): - self.name = name - self.client = client - self.changed = False - self.schedule_expression = schedule_expression - self.event_pattern = event_pattern - self.description = description - self.role_arn = role_arn - self.module = module - - def describe(self): - """Returns the existing details of the rule in AWS""" - try: - rule_info = self.client.describe_rule(Name=self.name) - except botocore.exceptions.ClientError as e: - error_code = e.response.get('Error', {}).get('Code') - if error_code == 'ResourceNotFoundException': - return {} - self.module.fail_json_aws(e, msg="Could not describe rule %s" % self.name) - except botocore.exceptions.BotoCoreError as e: - self.module.fail_json_aws(e, msg="Could not describe rule %s" % self.name) - return self._snakify(rule_info) - - def put(self, enabled=True): - """Creates or updates the rule in AWS""" - request = { - 'Name': self.name, - 'State': "ENABLED" if enabled else "DISABLED", - } - if self.schedule_expression: - request['ScheduleExpression'] = self.schedule_expression - if self.event_pattern: - request['EventPattern'] = self.event_pattern - if self.description: - request['Description'] = self.description - if self.role_arn: - request['RoleArn'] = self.role_arn - try: - response = self.client.put_rule(**request) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not create/update rule %s" % self.name) - self.changed = True - return response - - def delete(self): - """Deletes the rule in AWS""" - self.remove_all_targets() - - try: - response = self.client.delete_rule(Name=self.name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not delete rule %s" % self.name) - self.changed = True - return response - - def enable(self): - """Enables the rule in AWS""" - try: - response = self.client.enable_rule(Name=self.name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not enable rule %s" % self.name) - self.changed = True - return response - - def disable(self): - """Disables the rule in AWS""" - try: - response = self.client.disable_rule(Name=self.name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not disable rule %s" % self.name) - self.changed = True - return response - - def list_targets(self): - """Lists the existing targets for the rule in AWS""" - try: - targets = self.client.list_targets_by_rule(Rule=self.name) - except botocore.exceptions.ClientError as e: - error_code = e.response.get('Error', {}).get('Code') - if error_code == 'ResourceNotFoundException': - return [] - self.module.fail_json_aws(e, msg="Could not find target for rule %s" % self.name) - except botocore.exceptions.BotoCoreError as e: - self.module.fail_json_aws(e, msg="Could not find target for rule %s" % self.name) - return self._snakify(targets)['targets'] - - def put_targets(self, targets): - """Creates or updates the provided targets on the rule in AWS""" - if not targets: - return - request = { - 'Rule': self.name, - 'Targets': self._targets_request(targets), - } - try: - response = self.client.put_targets(**request) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not create/update rule targets for rule %s" % self.name) - self.changed = True - return response - - def remove_targets(self, target_ids): - """Removes the provided targets from the rule in AWS""" - if not target_ids: - return - request = { - 'Rule': self.name, - 'Ids': target_ids - } - try: - response = self.client.remove_targets(**request) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not remove rule targets from rule %s" % self.name) - self.changed = True - return response - - def remove_all_targets(self): - """Removes all targets on rule""" - targets = self.list_targets() - return self.remove_targets([t['id'] for t in targets]) - - def _targets_request(self, targets): - """Formats each target for the request""" - targets_request = [] - for target in targets: - target_request = { - 'Id': target['id'], - 'Arn': target['arn'] - } - if 'input' in target: - target_request['Input'] = target['input'] - if 'input_path' in target: - target_request['InputPath'] = target['input_path'] - if 'role_arn' in target: - target_request['RoleArn'] = target['role_arn'] - if 'ecs_parameters' in target: - target_request['EcsParameters'] = {} - ecs_parameters = target['ecs_parameters'] - if 'task_definition_arn' in target['ecs_parameters']: - target_request['EcsParameters']['TaskDefinitionArn'] = ecs_parameters['task_definition_arn'] - if 'task_count' in target['ecs_parameters']: - target_request['EcsParameters']['TaskCount'] = ecs_parameters['task_count'] - targets_request.append(target_request) - return targets_request - - def _snakify(self, dict): - """Converts camel case to snake case""" - return camel_dict_to_snake_dict(dict) - - -class CloudWatchEventRuleManager(object): - RULE_FIELDS = ['name', 'event_pattern', 'schedule_expression', 'description', 'role_arn'] - - def __init__(self, rule, targets): - self.rule = rule - self.targets = targets - - def ensure_present(self, enabled=True): - """Ensures the rule and targets are present and synced""" - rule_description = self.rule.describe() - if rule_description: - # Rule exists so update rule, targets and state - self._sync_rule(enabled) - self._sync_targets() - self._sync_state(enabled) - else: - # Rule does not exist, so create new rule and targets - self._create(enabled) - - def ensure_disabled(self): - """Ensures the rule and targets are present, but disabled, and synced""" - self.ensure_present(enabled=False) - - def ensure_absent(self): - """Ensures the rule and targets are absent""" - rule_description = self.rule.describe() - if not rule_description: - # Rule doesn't exist so don't need to delete - return - self.rule.delete() - - def fetch_aws_state(self): - """Retrieves rule and target state from AWS""" - aws_state = { - 'rule': {}, - 'targets': [], - 'changed': self.rule.changed - } - rule_description = self.rule.describe() - if not rule_description: - return aws_state - - # Don't need to include response metadata noise in response - del rule_description['response_metadata'] - - aws_state['rule'] = rule_description - aws_state['targets'].extend(self.rule.list_targets()) - return aws_state - - def _sync_rule(self, enabled=True): - """Syncs local rule state with AWS""" - if not self._rule_matches_aws(): - self.rule.put(enabled) - - def _sync_targets(self): - """Syncs local targets with AWS""" - # Identify and remove extraneous targets on AWS - target_ids_to_remove = self._remote_target_ids_to_remove() - if target_ids_to_remove: - self.rule.remove_targets(target_ids_to_remove) - - # Identify targets that need to be added or updated on AWS - targets_to_put = self._targets_to_put() - if targets_to_put: - self.rule.put_targets(targets_to_put) - - def _sync_state(self, enabled=True): - """Syncs local rule state with AWS""" - remote_state = self._remote_state() - if enabled and remote_state != 'ENABLED': - self.rule.enable() - elif not enabled and remote_state != 'DISABLED': - self.rule.disable() - - def _create(self, enabled=True): - """Creates rule and targets on AWS""" - self.rule.put(enabled) - self.rule.put_targets(self.targets) - - def _rule_matches_aws(self): - """Checks if the local rule data matches AWS""" - aws_rule_data = self.rule.describe() - - # The rule matches AWS only if all rule data fields are equal - # to their corresponding local value defined in the task - return all([ - getattr(self.rule, field) == aws_rule_data.get(field, None) - for field in self.RULE_FIELDS - ]) - - def _targets_to_put(self): - """Returns a list of targets that need to be updated or added remotely""" - remote_targets = self.rule.list_targets() - return [t for t in self.targets if t not in remote_targets] - - def _remote_target_ids_to_remove(self): - """Returns a list of targets that need to be removed remotely""" - target_ids = [t['id'] for t in self.targets] - remote_targets = self.rule.list_targets() - return [ - rt['id'] for rt in remote_targets if rt['id'] not in target_ids - ] - - def _remote_state(self): - """Returns the remote state from AWS""" - description = self.rule.describe() - if not description: - return - return description['state'] - - -def main(): - argument_spec = dict( - name=dict(required=True), - schedule_expression=dict(), - event_pattern=dict(), - state=dict(choices=['present', 'disabled', 'absent'], - default='present'), - description=dict(), - role_arn=dict(), - targets=dict(type='list', default=[]), - ) - module = AnsibleAWSModule(argument_spec=argument_spec) - - rule_data = dict( - [(rf, module.params.get(rf)) for rf in CloudWatchEventRuleManager.RULE_FIELDS] - ) - targets = module.params.get('targets') - state = module.params.get('state') - client = module.client('events') - - cwe_rule = CloudWatchEventRule(module, client=client, **rule_data) - cwe_rule_manager = CloudWatchEventRuleManager(cwe_rule, targets) - - if state == 'present': - cwe_rule_manager.ensure_present() - elif state == 'disabled': - cwe_rule_manager.ensure_disabled() - elif state == 'absent': - cwe_rule_manager.ensure_absent() - else: - module.fail_json(msg="Invalid state '{0}' provided".format(state)) - - module.exit_json(**cwe_rule_manager.fetch_aws_state()) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group.py b/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group.py deleted file mode 100644 index 7ffd6671a0..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group.py +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudwatchlogs_log_group -short_description: create or delete log_group in CloudWatchLogs -notes: - - For details of the parameters and returns see U(http://boto3.readthedocs.io/en/latest/reference/services/logs.html). -description: - - Create or delete log_group in CloudWatchLogs. -version_added: "2.5" -author: - - Willian Ricardo (@willricardo) <willricardo@gmail.com> -requirements: [ json, botocore, boto3 ] -options: - state: - description: - - Whether the rule is present or absent. - choices: ["present", "absent"] - default: present - required: false - type: str - log_group_name: - description: - - The name of the log group. - required: true - type: str - kms_key_id: - description: - - The Amazon Resource Name (ARN) of the CMK to use when encrypting log data. - required: false - type: str - tags: - description: - - The key-value pairs to use for the tags. - required: false - type: dict - retention: - description: - - The number of days to retain the log events in the specified log group. - - "Valid values are: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]" - - Mutually exclusive with I(purge_retention_policy). - required: false - type: int - purge_retention_policy: - description: - - "Whether to purge the retention policy or not." - - "Mutually exclusive with I(retention) and I(overwrite)." - default: false - required: false - type: bool - version_added: "2.10" - overwrite: - description: - - Whether an existing log group should be overwritten on create. - - Mutually exclusive with I(purge_retention_policy). - default: false - required: false - type: bool -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- cloudwatchlogs_log_group: - log_group_name: test-log-group - -- cloudwatchlogs_log_group: - state: present - log_group_name: test-log-group - tags: { "Name": "test-log-group", "Env" : "QA" } - -- cloudwatchlogs_log_group: - state: present - log_group_name: test-log-group - tags: { "Name": "test-log-group", "Env" : "QA" } - kms_key_id: arn:aws:kms:region:account-id:key/key-id - -- cloudwatchlogs_log_group: - state: absent - log_group_name: test-log-group - -''' - -RETURN = ''' -log_groups: - description: Return the list of complex objects representing log groups - returned: success - type: complex - contains: - log_group_name: - description: The name of the log group. - returned: always - type: str - creation_time: - description: The creation time of the log group. - returned: always - type: int - retention_in_days: - description: The number of days to retain the log events in the specified log group. - returned: always - type: int - metric_filter_count: - description: The number of metric filters. - returned: always - type: int - arn: - description: The Amazon Resource Name (ARN) of the log group. - returned: always - type: str - stored_bytes: - description: The number of bytes stored. - returned: always - type: str - kms_key_id: - description: The Amazon Resource Name (ARN) of the CMK to use when encrypting log data. - returned: always - type: str -''' - -import traceback -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, camel_dict_to_snake_dict, boto3_conn, ec2_argument_spec, get_aws_connection_info - -try: - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - - -def create_log_group(client, log_group_name, kms_key_id, tags, retention, module): - request = {'logGroupName': log_group_name} - if kms_key_id: - request['kmsKeyId'] = kms_key_id - if tags: - request['tags'] = tags - - try: - client.create_log_group(**request) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to create log group: {0}".format(to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to create log group: {0}".format(to_native(e)), - exception=traceback.format_exc()) - - if retention: - input_retention_policy(client=client, - log_group_name=log_group_name, - retention=retention, module=module) - - desc_log_group = describe_log_group(client=client, - log_group_name=log_group_name, - module=module) - - if 'logGroups' in desc_log_group: - for i in desc_log_group['logGroups']: - if log_group_name == i['logGroupName']: - return i - module.fail_json(msg="The aws CloudWatchLogs log group was not created. \n please try again!") - - -def input_retention_policy(client, log_group_name, retention, module): - try: - permited_values = [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] - - if retention in permited_values: - response = client.put_retention_policy(logGroupName=log_group_name, - retentionInDays=retention) - else: - delete_log_group(client=client, log_group_name=log_group_name, module=module) - module.fail_json(msg="Invalid retention value. Valid values are: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]") - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to put retention policy for log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to put retention policy for log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc()) - - -def delete_retention_policy(client, log_group_name, module): - try: - client.delete_retention_policy(logGroupName=log_group_name) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to delete retention policy for log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to delete retention policy for log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc()) - - -def delete_log_group(client, log_group_name, module): - desc_log_group = describe_log_group(client=client, - log_group_name=log_group_name, - module=module) - - try: - if 'logGroups' in desc_log_group: - for i in desc_log_group['logGroups']: - if log_group_name == i['logGroupName']: - client.delete_log_group(logGroupName=log_group_name) - - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to delete log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to delete log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc()) - - -def describe_log_group(client, log_group_name, module): - try: - desc_log_group = client.describe_log_groups(logGroupNamePrefix=log_group_name) - return desc_log_group - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to describe log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to describe log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc()) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - log_group_name=dict(required=True, type='str'), - state=dict(choices=['present', 'absent'], - default='present'), - kms_key_id=dict(required=False, type='str'), - tags=dict(required=False, type='dict'), - retention=dict(required=False, type='int'), - purge_retention_policy=dict(required=False, type='bool', default=False), - overwrite=dict(required=False, type='bool', default=False) - )) - - mutually_exclusive = [['retention', 'purge_retention_policy'], ['purge_retention_policy', 'overwrite']] - module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - logs = boto3_conn(module, conn_type='client', resource='logs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - - state = module.params.get('state') - changed = False - - # Determine if the log group exists - desc_log_group = describe_log_group(client=logs, log_group_name=module.params['log_group_name'], module=module) - found_log_group = {} - for i in desc_log_group.get('logGroups', []): - if module.params['log_group_name'] == i['logGroupName']: - found_log_group = i - break - - if state == 'present': - if found_log_group: - if module.params['overwrite'] is True: - changed = True - delete_log_group(client=logs, log_group_name=module.params['log_group_name'], module=module) - found_log_group = create_log_group(client=logs, - log_group_name=module.params['log_group_name'], - kms_key_id=module.params['kms_key_id'], - tags=module.params['tags'], - retention=module.params['retention'], - module=module) - elif module.params['purge_retention_policy']: - if found_log_group.get('retentionInDays'): - changed = True - delete_retention_policy(client=logs, - log_group_name=module.params['log_group_name'], - module=module) - elif module.params['retention'] != found_log_group.get('retentionInDays'): - if module.params['retention'] is not None: - changed = True - input_retention_policy(client=logs, - log_group_name=module.params['log_group_name'], - retention=module.params['retention'], - module=module) - found_log_group['retentionInDays'] = module.params['retention'] - - elif not found_log_group: - changed = True - found_log_group = create_log_group(client=logs, - log_group_name=module.params['log_group_name'], - kms_key_id=module.params['kms_key_id'], - tags=module.params['tags'], - retention=module.params['retention'], - module=module) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(found_log_group)) - - elif state == 'absent': - if found_log_group: - changed = True - delete_log_group(client=logs, - log_group_name=module.params['log_group_name'], - module=module) - - module.exit_json(changed=changed) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_info.py b/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_info.py deleted file mode 100644 index e098f28cc3..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_info.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: cloudwatchlogs_log_group_info -short_description: Get information about log_group in CloudWatchLogs -description: - - Lists the specified log groups. You can list all your log groups or filter the results by prefix. - - This module was called C(cloudwatchlogs_log_group_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -author: - - Willian Ricardo (@willricardo) <willricardo@gmail.com> -requirements: [ botocore, boto3 ] -options: - log_group_name: - description: - - The name or prefix of the log group to filter by. - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -- cloudwatchlogs_log_group_info: - log_group_name: test-log-group -''' - -RETURN = ''' -log_groups: - description: Return the list of complex objects representing log groups - returned: success - type: complex - contains: - log_group_name: - description: The name of the log group. - returned: always - type: str - creation_time: - description: The creation time of the log group. - returned: always - type: int - retention_in_days: - description: The number of days to retain the log events in the specified log group. - returned: always - type: int - metric_filter_count: - description: The number of metric filters. - returned: always - type: int - arn: - description: The Amazon Resource Name (ARN) of the log group. - returned: always - type: str - stored_bytes: - description: The number of bytes stored. - returned: always - type: str - kms_key_id: - description: The Amazon Resource Name (ARN) of the CMK to use when encrypting log data. - returned: always - type: str -''' - -import traceback -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, camel_dict_to_snake_dict, boto3_conn, ec2_argument_spec, get_aws_connection_info - -try: - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - - -def describe_log_group(client, log_group_name, module): - params = {} - if log_group_name: - params['logGroupNamePrefix'] = log_group_name - try: - paginator = client.get_paginator('describe_log_groups') - desc_log_group = paginator.paginate(**params).build_full_result() - return desc_log_group - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to describe log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to describe log group {0}: {1}".format(log_group_name, to_native(e)), - exception=traceback.format_exc()) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - log_group_name=dict(), - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'cloudwatchlogs_log_group_facts': - module.deprecate("The 'cloudwatchlogs_log_group_facts' module has been renamed to 'cloudwatchlogs_log_group_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - logs = boto3_conn(module, conn_type='client', resource='logs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - - desc_log_group = describe_log_group(client=logs, - log_group_name=module.params['log_group_name'], - module=module) - final_log_group_snake = [] - - for log_group in desc_log_group['logGroups']: - final_log_group_snake.append(camel_dict_to_snake_dict(log_group)) - - desc_log_group_result = dict(changed=False, log_groups=final_log_group_snake) - module.exit_json(**desc_log_group_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_metric_filter.py b/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_metric_filter.py deleted file mode 100644 index 9589259df7..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudwatchlogs_log_group_metric_filter.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: cloudwatchlogs_log_group_metric_filter -version_added: "2.10" -author: - - "Markus Bergholz (@markuman)" -short_description: Manage CloudWatch log group metric filter -description: - - Create, modify and delete CloudWatch log group metric filter. - - CloudWatch log group metric filter can be use with M(ec2_metric_alarm). -requirements: - - boto3 - - botocore -options: - state: - description: - - Whether the rule is present or absent. - choices: ["present", "absent"] - required: true - type: str - log_group_name: - description: - - The name of the log group where the metric filter is applied on. - required: true - type: str - filter_name: - description: - - A name for the metric filter you create. - required: true - type: str - filter_pattern: - description: - - A filter pattern for extracting metric data out of ingested log events. Required when I(state=present). - type: str - metric_transformation: - description: - - A collection of information that defines how metric data gets emitted. Required when I(state=present). - type: dict - suboptions: - metric_name: - description: - - The name of the cloudWatch metric. - type: str - metric_namespace: - description: - - The namespace of the cloudWatch metric. - type: str - metric_value: - description: - - The value to publish to the cloudWatch metric when a filter pattern matches a log event. - type: str - default_value: - description: - - The value to emit when a filter pattern does not match a log event. - type: float -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: set metric filter on log group /fluentd/testcase - cloudwatchlogs_log_group_metric_filter: - log_group_name: /fluentd/testcase - filter_name: BoxFreeStorage - filter_pattern: '{($.value = *) && ($.hostname = "box")}' - state: present - metric_transformation: - metric_name: box_free_space - metric_namespace: fluentd_metrics - metric_value: "$.value" - -- name: delete metric filter on log group /fluentd/testcase - cloudwatchlogs_log_group_metric_filter: - log_group_name: /fluentd/testcase - filter_name: BoxFreeStorage - state: absent -''' - -RETURN = """ -metric_filters: - description: Return the origin response value - returned: success - type: list - contains: - creation_time: - filter_name: - filter_pattern: - log_group_name: - metric_filter_count: -""" -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError -except ImportError: - pass # caught by AnsibleAWSModule - - -def metricTransformationHandler(metricTransformations, originMetricTransformations=None): - - if originMetricTransformations: - change = False - originMetricTransformations = camel_dict_to_snake_dict( - originMetricTransformations) - for item in ["default_value", "metric_name", "metric_namespace", "metric_value"]: - if metricTransformations.get(item) != originMetricTransformations.get(item): - change = True - else: - change = True - - defaultValue = metricTransformations.get("default_value") - if isinstance(defaultValue, int) or isinstance(defaultValue, float): - retval = [ - { - 'metricName': metricTransformations.get("metric_name"), - 'metricNamespace': metricTransformations.get("metric_namespace"), - 'metricValue': metricTransformations.get("metric_value"), - 'defaultValue': defaultValue - } - ] - else: - retval = [ - { - 'metricName': metricTransformations.get("metric_name"), - 'metricNamespace': metricTransformations.get("metric_namespace"), - 'metricValue': metricTransformations.get("metric_value"), - } - ] - - return retval, change - - -def main(): - - arg_spec = dict( - state=dict(type='str', required=True, choices=['present', 'absent']), - log_group_name=dict(type='str', required=True), - filter_name=dict(type='str', required=True), - filter_pattern=dict(type='str'), - metric_transformation=dict(type='dict', options=dict( - metric_name=dict(type='str'), - metric_namespace=dict(type='str'), - metric_value=dict(type='str'), - default_value=dict(type='float') - )), - ) - - module = AnsibleAWSModule( - argument_spec=arg_spec, - supports_check_mode=True, - required_if=[('state', 'present', ['metric_transformation', 'filter_pattern'])] - ) - - log_group_name = module.params.get("log_group_name") - filter_name = module.params.get("filter_name") - filter_pattern = module.params.get("filter_pattern") - metric_transformation = module.params.get("metric_transformation") - state = module.params.get("state") - - cwl = module.client('logs') - - # check if metric filter exists - response = cwl.describe_metric_filters( - logGroupName=log_group_name, - filterNamePrefix=filter_name - ) - - if len(response.get("metricFilters")) == 1: - originMetricTransformations = response.get( - "metricFilters")[0].get("metricTransformations")[0] - originFilterPattern = response.get("metricFilters")[ - 0].get("filterPattern") - else: - originMetricTransformations = None - originFilterPattern = None - change = False - metricTransformation = None - - if state == "absent" and originMetricTransformations: - if not module.check_mode: - response = cwl.delete_metric_filter( - logGroupName=log_group_name, - filterName=filter_name - ) - change = True - metricTransformation = [camel_dict_to_snake_dict(item) for item in [originMetricTransformations]] - - elif state == "present": - metricTransformation, change = metricTransformationHandler( - metricTransformations=metric_transformation, originMetricTransformations=originMetricTransformations) - - change = change or filter_pattern != originFilterPattern - - if change: - if not module.check_mode: - response = cwl.put_metric_filter( - logGroupName=log_group_name, - filterName=filter_name, - filterPattern=filter_pattern, - metricTransformations=metricTransformation - ) - - metricTransformation = [camel_dict_to_snake_dict(item) for item in metricTransformation] - - module.exit_json(changed=change, metric_filters=metricTransformation) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/data_pipeline.py b/lib/ansible/modules/cloud/amazon/data_pipeline.py deleted file mode 100644 index b687cfa600..0000000000 --- a/lib/ansible/modules/cloud/amazon/data_pipeline.py +++ /dev/null @@ -1,652 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: data_pipeline -version_added: "2.4" -author: - - Raghu Udiyar (@raags) <raghusiddarth@gmail.com> - - Sloane Hertel (@s-hertel) <shertel@redhat.com> -requirements: [ "boto3" ] -short_description: Create and manage AWS Datapipelines -extends_documentation_fragment: - - aws - - ec2 -description: - - Create and manage AWS Datapipelines. Creation is not idempotent in AWS, so the C(uniqueId) is created by hashing the options (minus objects) - given to the datapipeline. - - The pipeline definition must be in the format given here - U(https://docs.aws.amazon.com/datapipeline/latest/APIReference/API_PutPipelineDefinition.html#API_PutPipelineDefinition_RequestSyntax). - - Operations will wait for a configurable amount of time to ensure the pipeline is in the requested state. -options: - name: - description: - - The name of the Datapipeline to create/modify/delete. - required: true - type: str - description: - description: - - An optional description for the pipeline being created. - default: '' - type: str - objects: - type: list - elements: dict - description: - - A list of pipeline object definitions, each of which is a dict that takes the keys I(id), I(name) and I(fields). - suboptions: - id: - description: - - The ID of the object. - type: str - name: - description: - - The name of the object. - type: str - fields: - description: - - Key-value pairs that define the properties of the object. - - The value is specified as a reference to another object I(refValue) or as a string value I(stringValue) - but not as both. - type: list - elements: dict - suboptions: - key: - type: str - description: - - The field identifier. - stringValue: - type: str - description: - - The field value. - - Exactly one of I(stringValue) and I(refValue) may be specified. - refValue: - type: str - description: - - The field value, expressed as the identifier of another object. - - Exactly one of I(stringValue) and I(refValue) may be specified. - parameters: - description: - - A list of parameter objects (dicts) in the pipeline definition. - type: list - elements: dict - suboptions: - id: - description: - - The ID of the parameter object. - attributes: - description: - - A list of attributes (dicts) of the parameter object. - type: list - elements: dict - suboptions: - key: - description: The field identifier. - type: str - stringValue: - description: The field value. - type: str - - values: - description: - - A list of parameter values (dicts) in the pipeline definition. - type: list - elements: dict - suboptions: - id: - description: The ID of the parameter value - type: str - stringValue: - description: The field value - type: str - timeout: - description: - - Time in seconds to wait for the pipeline to transition to the requested state, fail otherwise. - default: 300 - type: int - state: - description: - - The requested state of the pipeline. - choices: ['present', 'absent', 'active', 'inactive'] - default: present - type: str - tags: - description: - - A dict of key:value pair(s) to add to the pipeline. - type: dict - version: - description: - - The version option has never had any effect and will be removed in - Ansible 2.14 - type: str -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create pipeline -- data_pipeline: - name: test-dp - region: us-west-2 - objects: "{{pipelineObjects}}" - parameters: "{{pipelineParameters}}" - values: "{{pipelineValues}}" - tags: - key1: val1 - key2: val2 - state: present - -# Example populating and activating a pipeline that demonstrates two ways of providing pipeline objects -- data_pipeline: - name: test-dp - objects: - - "id": "DefaultSchedule" - "name": "Every 1 day" - "fields": - - "key": "period" - "stringValue": "1 days" - - "key": "type" - "stringValue": "Schedule" - - "key": "startAt" - "stringValue": "FIRST_ACTIVATION_DATE_TIME" - - "id": "Default" - "name": "Default" - "fields": [ { "key": "resourceRole", "stringValue": "my_resource_role" }, - { "key": "role", "stringValue": "DataPipelineDefaultRole" }, - { "key": "pipelineLogUri", "stringValue": "s3://my_s3_log.txt" }, - { "key": "scheduleType", "stringValue": "cron" }, - { "key": "schedule", "refValue": "DefaultSchedule" }, - { "key": "failureAndRerunMode", "stringValue": "CASCADE" } ] - state: active - -# Activate pipeline -- data_pipeline: - name: test-dp - region: us-west-2 - state: active - -# Delete pipeline -- data_pipeline: - name: test-dp - region: us-west-2 - state: absent - -''' - -RETURN = ''' -changed: - description: whether the data pipeline has been modified - type: bool - returned: always - sample: - changed: true -result: - description: - - Contains the data pipeline data (data_pipeline) and a return message (msg). - If the data pipeline exists data_pipeline will contain the keys description, name, - pipeline_id, state, tags, and unique_id. If the data pipeline does not exist then - data_pipeline will be an empty dict. The msg describes the status of the operation. - returned: always - type: dict -''' - -import hashlib -import json -import time -import traceback - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn, camel_dict_to_snake_dict -from ansible.module_utils._text import to_text - - -DP_ACTIVE_STATES = ['ACTIVE', 'SCHEDULED'] -DP_INACTIVE_STATES = ['INACTIVE', 'PENDING', 'FINISHED', 'DELETING'] -DP_ACTIVATING_STATE = 'ACTIVATING' -DP_DEACTIVATING_STATE = 'DEACTIVATING' -PIPELINE_DOESNT_EXIST = '^.*Pipeline with id: {0} does not exist$' - - -class DataPipelineNotFound(Exception): - pass - - -class TimeOutException(Exception): - pass - - -def pipeline_id(client, name): - """Return pipeline id for the given pipeline name - - :param object client: boto3 datapipeline client - :param string name: pipeline name - :returns: pipeline id - :raises: DataPipelineNotFound - - """ - pipelines = client.list_pipelines() - for dp in pipelines['pipelineIdList']: - if dp['name'] == name: - return dp['id'] - raise DataPipelineNotFound - - -def pipeline_description(client, dp_id): - """Return pipeline description list - - :param object client: boto3 datapipeline client - :returns: pipeline description dictionary - :raises: DataPipelineNotFound - - """ - try: - return client.describe_pipelines(pipelineIds=[dp_id]) - except ClientError as e: - raise DataPipelineNotFound - - -def pipeline_field(client, dp_id, field): - """Return a pipeline field from the pipeline description. - - The available fields are listed in describe_pipelines output. - - :param object client: boto3 datapipeline client - :param string dp_id: pipeline id - :param string field: pipeline description field - :returns: pipeline field information - - """ - dp_description = pipeline_description(client, dp_id) - for field_key in dp_description['pipelineDescriptionList'][0]['fields']: - if field_key['key'] == field: - return field_key['stringValue'] - raise KeyError("Field key {0} not found!".format(field)) - - -def run_with_timeout(timeout, func, *func_args, **func_kwargs): - """Run func with the provided args and kwargs, and wait utill - timeout for truthy return value - - :param int timeout: time to wait for status - :param function func: function to run, should return True or False - :param args func_args: function args to pass to func - :param kwargs func_kwargs: function key word args - :returns: True if func returns truthy within timeout - :raises: TimeOutException - - """ - - for count in range(timeout // 10): - if func(*func_args, **func_kwargs): - return True - else: - # check every 10s - time.sleep(10) - - raise TimeOutException - - -def check_dp_exists(client, dp_id): - """Check if datapipeline exists - - :param object client: boto3 datapipeline client - :param string dp_id: pipeline id - :returns: True or False - - """ - try: - # pipeline_description raises DataPipelineNotFound - if pipeline_description(client, dp_id): - return True - else: - return False - except DataPipelineNotFound: - return False - - -def check_dp_status(client, dp_id, status): - """Checks if datapipeline matches states in status list - - :param object client: boto3 datapipeline client - :param string dp_id: pipeline id - :param list status: list of states to check against - :returns: True or False - - """ - if not isinstance(status, list): - raise AssertionError() - if pipeline_field(client, dp_id, field="@pipelineState") in status: - return True - else: - return False - - -def pipeline_status_timeout(client, dp_id, status, timeout): - args = (client, dp_id, status) - return run_with_timeout(timeout, check_dp_status, *args) - - -def pipeline_exists_timeout(client, dp_id, timeout): - args = (client, dp_id) - return run_with_timeout(timeout, check_dp_exists, *args) - - -def activate_pipeline(client, module): - """Activates pipeline - - """ - dp_name = module.params.get('name') - timeout = module.params.get('timeout') - - try: - dp_id = pipeline_id(client, dp_name) - except DataPipelineNotFound: - module.fail_json(msg='Data Pipeline {0} not found'.format(dp_name)) - - if pipeline_field(client, dp_id, field="@pipelineState") in DP_ACTIVE_STATES: - changed = False - else: - try: - client.activate_pipeline(pipelineId=dp_id) - except ClientError as e: - if e.response["Error"]["Code"] == "InvalidRequestException": - module.fail_json(msg="You need to populate your pipeline before activation.") - try: - pipeline_status_timeout(client, dp_id, status=DP_ACTIVE_STATES, - timeout=timeout) - except TimeOutException: - if pipeline_field(client, dp_id, field="@pipelineState") == "FINISHED": - # activated but completed more rapidly than it was checked - pass - else: - module.fail_json(msg=('Data Pipeline {0} failed to activate ' - 'within timeout {1} seconds').format(dp_name, timeout)) - changed = True - - data_pipeline = get_result(client, dp_id) - result = {'data_pipeline': data_pipeline, - 'msg': 'Data Pipeline {0} activated.'.format(dp_name)} - - return (changed, result) - - -def deactivate_pipeline(client, module): - """Deactivates pipeline - - """ - dp_name = module.params.get('name') - timeout = module.params.get('timeout') - - try: - dp_id = pipeline_id(client, dp_name) - except DataPipelineNotFound: - module.fail_json(msg='Data Pipeline {0} not found'.format(dp_name)) - - if pipeline_field(client, dp_id, field="@pipelineState") in DP_INACTIVE_STATES: - changed = False - else: - client.deactivate_pipeline(pipelineId=dp_id) - try: - pipeline_status_timeout(client, dp_id, status=DP_INACTIVE_STATES, - timeout=timeout) - except TimeOutException: - module.fail_json(msg=('Data Pipeline {0} failed to deactivate' - 'within timeout {1} seconds').format(dp_name, timeout)) - changed = True - - data_pipeline = get_result(client, dp_id) - result = {'data_pipeline': data_pipeline, - 'msg': 'Data Pipeline {0} deactivated.'.format(dp_name)} - - return (changed, result) - - -def _delete_dp_with_check(dp_id, client, timeout): - client.delete_pipeline(pipelineId=dp_id) - try: - pipeline_status_timeout(client=client, dp_id=dp_id, status=[PIPELINE_DOESNT_EXIST], timeout=timeout) - except DataPipelineNotFound: - return True - - -def delete_pipeline(client, module): - """Deletes pipeline - - """ - dp_name = module.params.get('name') - timeout = module.params.get('timeout') - - try: - dp_id = pipeline_id(client, dp_name) - _delete_dp_with_check(dp_id, client, timeout) - changed = True - except DataPipelineNotFound: - changed = False - except TimeOutException: - module.fail_json(msg=('Data Pipeline {0} failed to delete' - 'within timeout {1} seconds').format(dp_name, timeout)) - result = {'data_pipeline': {}, - 'msg': 'Data Pipeline {0} deleted'.format(dp_name)} - - return (changed, result) - - -def build_unique_id(module): - data = dict(module.params) - # removing objects from the unique id so we can update objects or populate the pipeline after creation without needing to make a new pipeline - [data.pop(each, None) for each in ('objects', 'timeout')] - json_data = json.dumps(data, sort_keys=True).encode("utf-8") - hashed_data = hashlib.md5(json_data).hexdigest() - return hashed_data - - -def format_tags(tags): - """ Reformats tags - - :param dict tags: dict of data pipeline tags (e.g. {key1: val1, key2: val2, key3: val3}) - :returns: list of dicts (e.g. [{key: key1, value: val1}, {key: key2, value: val2}, {key: key3, value: val3}]) - - """ - return [dict(key=k, value=v) for k, v in tags.items()] - - -def get_result(client, dp_id): - """ Get the current state of the data pipeline and reformat it to snake_case for exit_json - - :param object client: boto3 datapipeline client - :param string dp_id: pipeline id - :returns: reformatted dict of pipeline description - - """ - # pipeline_description returns a pipelineDescriptionList of length 1 - # dp is a dict with keys "description" (str), "fields" (list), "name" (str), "pipelineId" (str), "tags" (dict) - dp = pipeline_description(client, dp_id)['pipelineDescriptionList'][0] - - # Get uniqueId and pipelineState in fields to add to the exit_json result - dp["unique_id"] = pipeline_field(client, dp_id, field="uniqueId") - dp["pipeline_state"] = pipeline_field(client, dp_id, field="@pipelineState") - - # Remove fields; can't make a list snake_case and most of the data is redundant - del dp["fields"] - - # Note: tags is already formatted fine so we don't need to do anything with it - - # Reformat data pipeline and add reformatted fields back - dp = camel_dict_to_snake_dict(dp) - return dp - - -def diff_pipeline(client, module, objects, unique_id, dp_name): - """Check if there's another pipeline with the same unique_id and if so, checks if the object needs to be updated - """ - result = {} - changed = False - create_dp = False - - # See if there is already a pipeline with the same unique_id - unique_id = build_unique_id(module) - try: - dp_id = pipeline_id(client, dp_name) - dp_unique_id = to_text(pipeline_field(client, dp_id, field="uniqueId")) - if dp_unique_id != unique_id: - # A change is expected but not determined. Updated to a bool in create_pipeline(). - changed = "NEW_VERSION" - create_dp = True - # Unique ids are the same - check if pipeline needs modification - else: - dp_objects = client.get_pipeline_definition(pipelineId=dp_id)['pipelineObjects'] - # Definition needs to be updated - if dp_objects != objects: - changed, msg = define_pipeline(client, module, objects, dp_id) - # No changes - else: - msg = 'Data Pipeline {0} is present'.format(dp_name) - data_pipeline = get_result(client, dp_id) - result = {'data_pipeline': data_pipeline, - 'msg': msg} - except DataPipelineNotFound: - create_dp = True - - return create_dp, changed, result - - -def define_pipeline(client, module, objects, dp_id): - """Puts pipeline definition - - """ - dp_name = module.params.get('name') - - if pipeline_field(client, dp_id, field="@pipelineState") == "FINISHED": - msg = 'Data Pipeline {0} is unable to be updated while in state FINISHED.'.format(dp_name) - changed = False - - elif objects: - parameters = module.params.get('parameters') - values = module.params.get('values') - - try: - client.put_pipeline_definition(pipelineId=dp_id, - pipelineObjects=objects, - parameterObjects=parameters, - parameterValues=values) - msg = 'Data Pipeline {0} has been updated.'.format(dp_name) - changed = True - except ClientError as e: - module.fail_json(msg="Failed to put the definition for pipeline {0}. Check that string/reference fields" - "are not empty and that the number of objects in the pipeline does not exceed maximum allowed" - "objects".format(dp_name), exception=traceback.format_exc()) - else: - changed = False - msg = "" - - return changed, msg - - -def create_pipeline(client, module): - """Creates datapipeline. Uses uniqueId to achieve idempotency. - - """ - dp_name = module.params.get('name') - objects = module.params.get('objects', None) - description = module.params.get('description', '') - tags = module.params.get('tags') - timeout = module.params.get('timeout') - - unique_id = build_unique_id(module) - create_dp, changed, result = diff_pipeline(client, module, objects, unique_id, dp_name) - - if changed == "NEW_VERSION": - # delete old version - changed, creation_result = delete_pipeline(client, module) - - # There isn't a pipeline or it has different parameters than the pipeline in existence. - if create_dp: - # Make pipeline - try: - tags = format_tags(tags) - dp = client.create_pipeline(name=dp_name, - uniqueId=unique_id, - description=description, - tags=tags) - dp_id = dp['pipelineId'] - pipeline_exists_timeout(client, dp_id, timeout) - except ClientError as e: - module.fail_json(msg="Failed to create the data pipeline {0}.".format(dp_name), exception=traceback.format_exc()) - except TimeOutException: - module.fail_json(msg=('Data Pipeline {0} failed to create' - 'within timeout {1} seconds').format(dp_name, timeout)) - # Put pipeline definition - changed, msg = define_pipeline(client, module, objects, dp_id) - - changed = True - data_pipeline = get_result(client, dp_id) - result = {'data_pipeline': data_pipeline, - 'msg': 'Data Pipeline {0} created.'.format(dp_name) + msg} - - return (changed, result) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True), - version=dict(removed_in_version='2.14'), - description=dict(required=False, default=''), - objects=dict(required=False, type='list', default=[]), - parameters=dict(required=False, type='list', default=[]), - timeout=dict(required=False, type='int', default=300), - state=dict(default='present', choices=['present', 'absent', - 'active', 'inactive']), - tags=dict(required=False, type='dict', default={}), - values=dict(required=False, type='list', default=[]) - ) - ) - module = AnsibleModule(argument_spec, supports_check_mode=False) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for the datapipeline module!') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - client = boto3_conn(module, conn_type='client', - resource='datapipeline', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - except ClientError as e: - module.fail_json(msg="Can't authorize connection - " + str(e)) - - state = module.params.get('state') - if state == 'present': - changed, result = create_pipeline(client, module) - elif state == 'absent': - changed, result = delete_pipeline(client, module) - elif state == 'active': - changed, result = activate_pipeline(client, module) - elif state == 'inactive': - changed, result = deactivate_pipeline(client, module) - - module.exit_json(result=result, changed=changed) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/dms_endpoint.py b/lib/ansible/modules/cloud/amazon/dms_endpoint.py deleted file mode 100644 index 992bf4df06..0000000000 --- a/lib/ansible/modules/cloud/amazon/dms_endpoint.py +++ /dev/null @@ -1,472 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: dms_endpoint -short_description: Creates or destroys a data migration services endpoint -description: - - Creates or destroys a data migration services endpoint, - that can be used to replicate data. -version_added: "2.9" -options: - state: - description: - - State of the endpoint. - default: present - choices: ['present', 'absent'] - type: str - endpointidentifier: - description: - - An identifier name for the endpoint. - type: str - required: true - endpointtype: - description: - - Type of endpoint we want to manage. - choices: ['source', 'target'] - type: str - required: true - enginename: - description: - - Database engine that we want to use, please refer to - the AWS DMS for more information on the supported - engines and their limitations. - choices: ['mysql', 'oracle', 'postgres', 'mariadb', 'aurora', - 'redshift', 's3', 'db2', 'azuredb', 'sybase', - 'dynamodb', 'mongodb', 'sqlserver'] - type: str - required: true - username: - description: - - Username our endpoint will use to connect to the database. - type: str - password: - description: - - Password used to connect to the database - this attribute can only be written - the AWS API does not return this parameter. - type: str - servername: - description: - - Servername that the endpoint will connect to. - type: str - port: - description: - - TCP port for access to the database. - type: int - databasename: - description: - - Name for the database on the origin or target side. - type: str - extraconnectionattributes: - description: - - Extra attributes for the database connection, the AWS documentation - states " For more information about extra connection attributes, - see the documentation section for your data store." - type: str - kmskeyid: - description: - - Encryption key to use to encrypt replication storage and - connection information. - type: str - tags: - description: - - A list of tags to add to the endpoint. - type: dict - certificatearn: - description: - - Amazon Resource Name (ARN) for the certificate. - type: str - sslmode: - description: - - Mode used for the SSL connection. - default: none - choices: ['none', 'require', 'verify-ca', 'verify-full'] - type: str - serviceaccessrolearn: - description: - - Amazon Resource Name (ARN) for the service access role that you - want to use to create the endpoint. - type: str - externaltabledefinition: - description: - - The external table definition. - type: str - dynamodbsettings: - description: - - Settings in JSON format for the target Amazon DynamoDB endpoint - if source or target is dynamodb. - type: dict - s3settings: - description: - - S3 buckets settings for the target Amazon S3 endpoint. - type: dict - dmstransfersettings: - description: - - The settings in JSON format for the DMS transfer type of - source endpoint. - type: dict - mongodbsettings: - description: - - Settings in JSON format for the source MongoDB endpoint. - type: dict - kinesissettings: - description: - - Settings in JSON format for the target Amazon Kinesis - Data Streams endpoint. - type: dict - elasticsearchsettings: - description: - - Settings in JSON format for the target Elasticsearch endpoint. - type: dict - wait: - description: - - Whether Ansible should wait for the object to be deleted when I(state=absent). - type: bool - default: false - timeout: - description: - - Time in seconds we should wait for when deleting a resource. - - Required when I(wait=true). - type: int - retries: - description: - - number of times we should retry when deleting a resource - - Required when I(wait=true). - type: int -author: - - "Rui Moreira (@ruimoreira)" -extends_documentation_fragment: -- aws -- ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details -# Endpoint Creation -- dms_endpoint: - state: absent - endpointidentifier: 'testsource' - endpointtype: source - enginename: aurora - username: testing1 - password: testint1234 - servername: testing.domain.com - port: 3306 - databasename: 'testdb' - sslmode: none - wait: false -''' - -RETURN = ''' # ''' -__metaclass__ = type -import traceback -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -backoff_params = dict(tries=5, delay=1, backoff=1.5) - - -@AWSRetry.backoff(**backoff_params) -def describe_endpoints(connection, endpoint_identifier): - """ checks if the endpoint exists """ - try: - endpoint_filter = dict(Name='endpoint-id', - Values=[endpoint_identifier]) - return connection.describe_endpoints(Filters=[endpoint_filter]) - except botocore.exceptions.ClientError: - return {'Endpoints': []} - - -@AWSRetry.backoff(**backoff_params) -def dms_delete_endpoint(client, **params): - """deletes the DMS endpoint based on the EndpointArn""" - if module.params.get('wait'): - return delete_dms_endpoint(client) - else: - return client.delete_endpoint(**params) - - -@AWSRetry.backoff(**backoff_params) -def dms_create_endpoint(client, **params): - """ creates the DMS endpoint""" - return client.create_endpoint(**params) - - -@AWSRetry.backoff(**backoff_params) -def dms_modify_endpoint(client, **params): - """ updates the endpoint""" - return client.modify_endpoint(**params) - - -@AWSRetry.backoff(**backoff_params) -def get_endpoint_deleted_waiter(client): - return client.get_waiter('endpoint_deleted') - - -def endpoint_exists(endpoint): - """ Returns boolean based on the existence of the endpoint - :param endpoint: dict containing the described endpoint - :return: bool - """ - return bool(len(endpoint['Endpoints'])) - - -def delete_dms_endpoint(connection): - try: - endpoint = describe_endpoints(connection, - module.params.get('endpointidentifier')) - endpoint_arn = endpoint['Endpoints'][0].get('EndpointArn') - delete_arn = dict( - EndpointArn=endpoint_arn - ) - if module.params.get('wait'): - - delete_output = connection.delete_endpoint(**delete_arn) - delete_waiter = get_endpoint_deleted_waiter(connection) - delete_waiter.wait( - Filters=[{ - 'Name': 'endpoint-arn', - 'Values': [endpoint_arn] - - }], - WaiterConfig={ - 'Delay': module.params.get('timeout'), - 'MaxAttempts': module.params.get('retries') - } - ) - return delete_output - else: - return connection.delete_endpoint(**delete_arn) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to delete the DMS endpoint.", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to delete the DMS endpoint.", - exception=traceback.format_exc()) - - -def create_module_params(): - """ - Reads the module parameters and returns a dict - :return: dict - """ - endpoint_parameters = dict( - EndpointIdentifier=module.params.get('endpointidentifier'), - EndpointType=module.params.get('endpointtype'), - EngineName=module.params.get('enginename'), - Username=module.params.get('username'), - Password=module.params.get('password'), - ServerName=module.params.get('servername'), - Port=module.params.get('port'), - DatabaseName=module.params.get('databasename'), - SslMode=module.params.get('sslmode') - ) - if module.params.get('EndpointArn'): - endpoint_parameters['EndpointArn'] = module.params.get('EndpointArn') - if module.params.get('certificatearn'): - endpoint_parameters['CertificateArn'] = \ - module.params.get('certificatearn') - - if module.params.get('dmstransfersettings'): - endpoint_parameters['DmsTransferSettings'] = \ - module.params.get('dmstransfersettings') - - if module.params.get('extraconnectionattributes'): - endpoint_parameters['ExtraConnectionAttributes'] =\ - module.params.get('extraconnectionattributes') - - if module.params.get('kmskeyid'): - endpoint_parameters['KmsKeyId'] = module.params.get('kmskeyid') - - if module.params.get('tags'): - endpoint_parameters['Tags'] = module.params.get('tags') - - if module.params.get('serviceaccessrolearn'): - endpoint_parameters['ServiceAccessRoleArn'] = \ - module.params.get('serviceaccessrolearn') - - if module.params.get('externaltabledefinition'): - endpoint_parameters['ExternalTableDefinition'] = \ - module.params.get('externaltabledefinition') - - if module.params.get('dynamodbsettings'): - endpoint_parameters['DynamoDbSettings'] = \ - module.params.get('dynamodbsettings') - - if module.params.get('s3settings'): - endpoint_parameters['S3Settings'] = module.params.get('s3settings') - - if module.params.get('mongodbsettings'): - endpoint_parameters['MongoDbSettings'] = \ - module.params.get('mongodbsettings') - - if module.params.get('kinesissettings'): - endpoint_parameters['KinesisSettings'] = \ - module.params.get('kinesissettings') - - if module.params.get('elasticsearchsettings'): - endpoint_parameters['ElasticsearchSettings'] = \ - module.params.get('elasticsearchsettings') - - if module.params.get('wait'): - endpoint_parameters['wait'] = module.boolean(module.params.get('wait')) - - if module.params.get('timeout'): - endpoint_parameters['timeout'] = module.params.get('timeout') - - if module.params.get('retries'): - endpoint_parameters['retries'] = module.params.get('retries') - - return endpoint_parameters - - -def compare_params(param_described): - """ - Compares the dict obtained from the describe DMS endpoint and - what we are reading from the values in the template We can - never compare the password as boto3's method for describing - a DMS endpoint does not return the value for - the password for security reasons ( I assume ) - """ - modparams = create_module_params() - changed = False - for paramname in modparams: - if paramname == 'Password' or paramname in param_described \ - and param_described[paramname] == modparams[paramname] or \ - str(param_described[paramname]).lower() \ - == modparams[paramname]: - pass - else: - changed = True - return changed - - -def modify_dms_endpoint(connection): - - try: - params = create_module_params() - return dms_modify_endpoint(connection, **params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to update DMS endpoint.", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to update DMS endpoint.", - exception=traceback.format_exc()) - - -def create_dms_endpoint(connection): - """ - Function to create the dms endpoint - :param connection: boto3 aws connection - :return: information about the dms endpoint object - """ - - try: - params = create_module_params() - return dms_create_endpoint(connection, **params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to create DMS endpoint.", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to create DMS endpoint.", - exception=traceback.format_exc()) - - -def main(): - argument_spec = dict( - state=dict(choices=['present', 'absent'], default='present'), - endpointidentifier=dict(required=True), - endpointtype=dict(choices=['source', 'target'], required=True), - enginename=dict(choices=['mysql', 'oracle', 'postgres', 'mariadb', - 'aurora', 'redshift', 's3', 'db2', 'azuredb', - 'sybase', 'dynamodb', 'mongodb', 'sqlserver'], - required=True), - username=dict(), - password=dict(no_log=True), - servername=dict(), - port=dict(type='int'), - databasename=dict(), - extraconnectionattributes=dict(), - kmskeyid=dict(), - tags=dict(type='dict'), - certificatearn=dict(), - sslmode=dict(choices=['none', 'require', 'verify-ca', 'verify-full'], - default='none'), - serviceaccessrolearn=dict(), - externaltabledefinition=dict(), - dynamodbsettings=dict(type='dict'), - s3settings=dict(type='dict'), - dmstransfersettings=dict(type='dict'), - mongodbsettings=dict(type='dict'), - kinesissettings=dict(type='dict'), - elasticsearchsettings=dict(type='dict'), - wait=dict(type='bool', default=False), - timeout=dict(type='int'), - retries=dict(type='int') - ) - global module - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_if=[ - ["state", "absent", ["wait"]], - ["wait", "True", ["timeout"]], - ["wait", "True", ["retries"]], - ], - supports_check_mode=False - ) - exit_message = None - changed = False - - state = module.params.get('state') - - dmsclient = module.client('dms') - endpoint = describe_endpoints(dmsclient, - module.params.get('endpointidentifier')) - if state == 'present': - if endpoint_exists(endpoint): - module.params['EndpointArn'] = \ - endpoint['Endpoints'][0].get('EndpointArn') - params_changed = compare_params(endpoint["Endpoints"][0]) - if params_changed: - updated_dms = modify_dms_endpoint(dmsclient) - exit_message = updated_dms - changed = True - else: - module.exit_json(changed=False, msg="Endpoint Already Exists") - else: - dms_properties = create_dms_endpoint(dmsclient) - exit_message = dms_properties - changed = True - elif state == 'absent': - if endpoint_exists(endpoint): - delete_results = delete_dms_endpoint(dmsclient) - exit_message = delete_results - changed = True - else: - changed = False - exit_message = 'DMS Endpoint does not exist' - - module.exit_json(changed=changed, msg=exit_message) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/dms_replication_subnet_group.py b/lib/ansible/modules/cloud/amazon/dms_replication_subnet_group.py deleted file mode 100644 index 97d0567d01..0000000000 --- a/lib/ansible/modules/cloud/amazon/dms_replication_subnet_group.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: dms_replication_subnet_group -short_description: creates or destroys a data migration services subnet group -description: - - Creates or destroys a data migration services subnet group. -version_added: "2.9" -options: - state: - description: - - State of the subnet group. - default: present - choices: ['present', 'absent'] - type: str - identifier: - description: - - The name for the replication subnet group. - This value is stored as a lowercase string. - Must contain no more than 255 alphanumeric characters, - periods, spaces, underscores, or hyphens. Must not be "default". - type: str - required: true - description: - description: - - The description for the subnet group. - type: str - required: true - subnet_ids: - description: - - A list containing the subnet ids for the replication subnet group, - needs to be at least 2 items in the list. - type: list - elements: str - required: true -author: - - "Rui Moreira (@ruimoreira)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- dms_replication_subnet_group: - state: present - identifier: "dev-sngroup" - description: "Development Subnet Group asdasdas" - subnet_ids: ['subnet-id1','subnet-id2'] -''' - -RETURN = ''' # ''' - -import traceback -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -backoff_params = dict(tries=5, delay=1, backoff=1.5) - - -@AWSRetry.backoff(**backoff_params) -def describe_subnet_group(connection, subnet_group): - """checks if instance exists""" - try: - subnet_group_filter = dict(Name='replication-subnet-group-id', - Values=[subnet_group]) - return connection.describe_replication_subnet_groups(Filters=[subnet_group_filter]) - except botocore.exceptions.ClientError: - return {'ReplicationSubnetGroups': []} - - -@AWSRetry.backoff(**backoff_params) -def replication_subnet_group_create(connection, **params): - """ creates the replication subnet group """ - return connection.create_replication_subnet_group(**params) - - -@AWSRetry.backoff(**backoff_params) -def replication_subnet_group_modify(connection, **modify_params): - return connection.modify_replication_subnet_group(**modify_params) - - -@AWSRetry.backoff(**backoff_params) -def replication_subnet_group_delete(module, connection): - subnetid = module.params.get('identifier') - delete_parameters = dict(ReplicationSubnetGroupIdentifier=subnetid) - return connection.delete_replication_subnet_group(**delete_parameters) - - -def replication_subnet_exists(subnet): - """ Returns boolean based on the existence of the endpoint - :param endpoint: dict containing the described endpoint - :return: bool - """ - return bool(len(subnet['ReplicationSubnetGroups'])) - - -def create_module_params(module): - """ - Reads the module parameters and returns a dict - :return: dict - """ - instance_parameters = dict( - # ReplicationSubnetGroupIdentifier gets translated to lower case anyway by the API - ReplicationSubnetGroupIdentifier=module.params.get('identifier').lower(), - ReplicationSubnetGroupDescription=module.params.get('description'), - SubnetIds=module.params.get('subnet_ids'), - ) - - return instance_parameters - - -def compare_params(module, param_described): - """ - Compares the dict obtained from the describe function and - what we are reading from the values in the template We can - never compare passwords as boto3's method for describing - a DMS endpoint does not return the value for - the password for security reasons ( I assume ) - """ - modparams = create_module_params(module) - changed = False - # need to sanitize values that get returned from the API - if 'VpcId' in param_described.keys(): - param_described.pop('VpcId') - if 'SubnetGroupStatus' in param_described.keys(): - param_described.pop('SubnetGroupStatus') - for paramname in modparams.keys(): - if paramname in param_described.keys() and \ - param_described.get(paramname) == modparams[paramname]: - pass - elif paramname == 'SubnetIds': - subnets = [] - for subnet in param_described.get('Subnets'): - subnets.append(subnet.get('SubnetIdentifier')) - for modulesubnet in modparams['SubnetIds']: - if modulesubnet in subnets: - pass - else: - changed = True - return changed - - -def create_replication_subnet_group(module, connection): - try: - params = create_module_params(module) - return replication_subnet_group_create(connection, **params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to create DMS replication subnet group.", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to create DMS replication subnet group.", - exception=traceback.format_exc()) - - -def modify_replication_subnet_group(module, connection): - try: - modify_params = create_module_params(module) - return replication_subnet_group_modify(connection, **modify_params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to Modify the DMS replication subnet group.", - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to Modify the DMS replication subnet group.", - exception=traceback.format_exc()) - - -def main(): - argument_spec = dict( - state=dict(type='str', choices=['present', 'absent'], default='present'), - identifier=dict(type='str', required=True), - description=dict(type='str', required=True), - subnet_ids=dict(type='list', elements='str', required=True), - ) - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - exit_message = None - changed = False - - state = module.params.get('state') - dmsclient = module.client('dms') - subnet_group = describe_subnet_group(dmsclient, - module.params.get('identifier')) - if state == 'present': - if replication_subnet_exists(subnet_group): - if compare_params(module, subnet_group["ReplicationSubnetGroups"][0]): - if not module.check_mode: - exit_message = modify_replication_subnet_group(module, dmsclient) - else: - exit_message = dmsclient - changed = True - else: - exit_message = "No changes to Subnet group" - else: - if not module.check_mode: - exit_message = create_replication_subnet_group(module, dmsclient) - changed = True - else: - exit_message = "Check mode enabled" - - elif state == 'absent': - if replication_subnet_exists(subnet_group): - if not module.check_mode: - replication_subnet_group_delete(module, dmsclient) - changed = True - exit_message = "Replication subnet group Deleted" - else: - exit_message = dmsclient - changed = True - - else: - changed = False - exit_message = "Replication subnet group does not exist" - - module.exit_json(changed=changed, msg=exit_message) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/dynamodb_table.py b/lib/ansible/modules/cloud/amazon/dynamodb_table.py deleted file mode 100644 index 4e4a863643..0000000000 --- a/lib/ansible/modules/cloud/amazon/dynamodb_table.py +++ /dev/null @@ -1,522 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: dynamodb_table -short_description: Create, update or delete AWS Dynamo DB tables -version_added: "2.0" -description: - - Create or delete AWS Dynamo DB tables. - - Can update the provisioned throughput on existing tables. - - Returns the status of the specified table. -author: Alan Loi (@loia) -requirements: - - "boto >= 2.37.0" - - "boto3 >= 1.4.4 (for tagging)" -options: - state: - description: - - Create or delete the table. - choices: ['present', 'absent'] - default: 'present' - type: str - name: - description: - - Name of the table. - required: true - type: str - hash_key_name: - description: - - Name of the hash key. - - Required when C(state=present). - type: str - hash_key_type: - description: - - Type of the hash key. - choices: ['STRING', 'NUMBER', 'BINARY'] - default: 'STRING' - type: str - range_key_name: - description: - - Name of the range key. - type: str - range_key_type: - description: - - Type of the range key. - choices: ['STRING', 'NUMBER', 'BINARY'] - default: 'STRING' - type: str - read_capacity: - description: - - Read throughput capacity (units) to provision. - default: 1 - type: int - write_capacity: - description: - - Write throughput capacity (units) to provision. - default: 1 - type: int - indexes: - description: - - list of dictionaries describing indexes to add to the table. global indexes can be updated. local indexes don't support updates or have throughput. - - "required options: ['name', 'type', 'hash_key_name']" - - "other options: ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity']" - suboptions: - name: - description: The name of the index. - type: str - required: true - type: - description: - - The type of index. - - "Valid types: C(all), C(global_all), C(global_include), C(global_keys_only), C(include), C(keys_only)" - type: str - required: true - hash_key_name: - description: The name of the hash-based key. - required: true - type: str - hash_key_type: - description: The type of the hash-based key. - type: str - range_key_name: - description: The name of the range-based key. - type: str - range_key_type: - type: str - description: The type of the range-based key. - includes: - type: list - description: A list of fields to include when using C(global_include) or C(include) indexes. - read_capacity: - description: - - Read throughput capacity (units) to provision for the index. - type: int - write_capacity: - description: - - Write throughput capacity (units) to provision for the index. - type: int - default: [] - version_added: "2.1" - type: list - elements: dict - tags: - version_added: "2.4" - description: - - A hash/dictionary of tags to add to the new instance or for starting/stopping instance by tag. - - 'For example: C({"key":"value"}) and C({"key":"value","key2":"value2"})' - type: dict - wait_for_active_timeout: - version_added: "2.4" - description: - - how long before wait gives up, in seconds. only used when tags is set - default: 60 - type: int -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = ''' -# Create dynamo table with hash and range primary key -- dynamodb_table: - name: my-table - region: us-east-1 - hash_key_name: id - hash_key_type: STRING - range_key_name: create_time - range_key_type: NUMBER - read_capacity: 2 - write_capacity: 2 - tags: - tag_name: tag_value - -# Update capacity on existing dynamo table -- dynamodb_table: - name: my-table - region: us-east-1 - read_capacity: 10 - write_capacity: 10 - -# set index on existing dynamo table -- dynamodb_table: - name: my-table - region: us-east-1 - indexes: - - name: NamedIndex - type: global_include - hash_key_name: id - range_key_name: create_time - includes: - - other_field - - other_field2 - read_capacity: 10 - write_capacity: 10 - -# Delete dynamo table -- dynamodb_table: - name: my-table - region: us-east-1 - state: absent -''' - -RETURN = ''' -table_status: - description: The current status of the table. - returned: success - type: str - sample: ACTIVE -''' - -import time -import traceback - -try: - import boto - import boto.dynamodb2 - from boto.dynamodb2.table import Table - from boto.dynamodb2.fields import HashKey, RangeKey, AllIndex, GlobalAllIndex, GlobalIncludeIndex, GlobalKeysOnlyIndex, IncludeIndex, KeysOnlyIndex - from boto.dynamodb2.types import STRING, NUMBER, BINARY - from boto.exception import BotoServerError, NoAuthHandlerFound, JSONResponseError - from boto.dynamodb2.exceptions import ValidationException - HAS_BOTO = True - - DYNAMO_TYPE_MAP = { - 'STRING': STRING, - 'NUMBER': NUMBER, - 'BINARY': BINARY - } - -except ImportError: - HAS_BOTO = False - -try: - import botocore - from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_conn - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info - - -DYNAMO_TYPE_DEFAULT = 'STRING' -INDEX_REQUIRED_OPTIONS = ['name', 'type', 'hash_key_name'] -INDEX_OPTIONS = INDEX_REQUIRED_OPTIONS + ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity'] -INDEX_TYPE_OPTIONS = ['all', 'global_all', 'global_include', 'global_keys_only', 'include', 'keys_only'] - - -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: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) - - -def get_account_id(boto3_sts): - return boto3_sts.get_caller_identity()["Account"] - - -def wait_until_table_active(module, table, wait_timeout): - max_wait_time = time.time() + wait_timeout - while (max_wait_time > time.time()) and (table.describe()['Table']['TableStatus'] != 'ACTIVE'): - time.sleep(5) - if max_wait_time <= time.time(): - # waiting took too long - module.fail_json(msg="timed out waiting for table to exist") - - -def delete_dynamo_table(connection, module): - table_name = module.params.get('name') - - result = dict( - region=module.params.get('region'), - table_name=table_name, - ) - - try: - table = Table(table_name, connection=connection) - - if dynamo_table_exists(table): - if not module.check_mode: - table.delete() - result['changed'] = True - - else: - result['changed'] = False - - except BotoServerError: - result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc() - module.fail_json(**result) - else: - module.exit_json(**result) - - -def dynamo_table_exists(table): - try: - table.describe() - return True - - except JSONResponseError as e: - if e.message and e.message.startswith('Requested resource not found'): - return False - else: - raise e - - -def update_dynamo_table(table, throughput=None, check_mode=False, global_indexes=None): - table.describe() # populate table details - throughput_changed = False - global_indexes_changed = False - if has_throughput_changed(table, throughput): - if not check_mode: - throughput_changed = table.update(throughput=throughput) - else: - throughput_changed = True - - removed_indexes, added_indexes, index_throughput_changes = get_changed_global_indexes(table, global_indexes) - if removed_indexes: - if not check_mode: - for name, index in removed_indexes.items(): - global_indexes_changed = table.delete_global_secondary_index(name) or global_indexes_changed - else: - global_indexes_changed = True - - if added_indexes: - if not check_mode: - for name, index in added_indexes.items(): - global_indexes_changed = table.create_global_secondary_index(global_index=index) or global_indexes_changed - else: - global_indexes_changed = True - - if index_throughput_changes: - if not check_mode: - # todo: remove try once boto has https://github.com/boto/boto/pull/3447 fixed - try: - global_indexes_changed = table.update_global_secondary_index(global_indexes=index_throughput_changes) or global_indexes_changed - except ValidationException: - pass - else: - global_indexes_changed = True - - return throughput_changed or global_indexes_changed - - -def has_throughput_changed(table, new_throughput): - if not new_throughput: - return False - - return new_throughput['read'] != table.throughput['read'] or \ - new_throughput['write'] != table.throughput['write'] - - -def get_schema_param(hash_key_name, hash_key_type, range_key_name, range_key_type): - if range_key_name: - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])), - RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])) - ] - else: - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])) - ] - return schema - - -def get_changed_global_indexes(table, global_indexes): - table.describe() - - table_index_info = dict((index.name, index.schema()) for index in table.global_indexes) - table_index_objects = dict((index.name, index) for index in table.global_indexes) - set_index_info = dict((index.name, index.schema()) for index in global_indexes) - set_index_objects = dict((index.name, index) for index in global_indexes) - - removed_indexes = dict((name, index) for name, index in table_index_info.items() if name not in set_index_info) - added_indexes = dict((name, set_index_objects[name]) for name, index in set_index_info.items() if name not in table_index_info) - # todo: uncomment once boto has https://github.com/boto/boto/pull/3447 fixed - # for name, index in set_index_objects.items(): - # if (name not in added_indexes and - # (index.throughput['read'] != str(table_index_objects[name].throughput['read']) or - # index.throughput['write'] != str(table_index_objects[name].throughput['write']))): - # index_throughput_changes[name] = index.throughput - # todo: remove once boto has https://github.com/boto/boto/pull/3447 fixed - index_throughput_changes = dict((name, index.throughput) for name, index in set_index_objects.items() if name not in added_indexes) - - return removed_indexes, added_indexes, index_throughput_changes - - -def validate_index(index, module): - for key, val in index.items(): - if key not in INDEX_OPTIONS: - module.fail_json(msg='%s is not a valid option for an index' % key) - for required_option in INDEX_REQUIRED_OPTIONS: - if required_option not in index: - module.fail_json(msg='%s is a required option for an index' % required_option) - if index['type'] not in INDEX_TYPE_OPTIONS: - module.fail_json(msg='%s is not a valid index type, must be one of %s' % (index['type'], INDEX_TYPE_OPTIONS)) - - -def get_indexes(all_indexes): - indexes = [] - global_indexes = [] - for index in all_indexes: - name = index['name'] - schema = get_schema_param(index.get('hash_key_name'), index.get('hash_key_type'), index.get('range_key_name'), index.get('range_key_type')) - throughput = { - 'read': index.get('read_capacity', 1), - 'write': index.get('write_capacity', 1) - } - - if index['type'] == 'all': - indexes.append(AllIndex(name, parts=schema)) - - elif index['type'] == 'global_all': - global_indexes.append(GlobalAllIndex(name, parts=schema, throughput=throughput)) - - elif index['type'] == 'global_include': - global_indexes.append(GlobalIncludeIndex(name, parts=schema, throughput=throughput, includes=index['includes'])) - - elif index['type'] == 'global_keys_only': - global_indexes.append(GlobalKeysOnlyIndex(name, parts=schema, throughput=throughput)) - - elif index['type'] == 'include': - indexes.append(IncludeIndex(name, parts=schema, includes=index['includes'])) - - elif index['type'] == 'keys_only': - indexes.append(KeysOnlyIndex(name, parts=schema)) - - return indexes, global_indexes - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(default='present', choices=['present', 'absent']), - name=dict(required=True, type='str'), - hash_key_name=dict(type='str'), - hash_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), - range_key_name=dict(type='str'), - range_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), - read_capacity=dict(default=1, type='int'), - write_capacity=dict(default=1, type='int'), - indexes=dict(default=[], type='list'), - tags=dict(type='dict'), - wait_for_active_timeout=dict(default=60, type='int'), - )) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - if not HAS_BOTO3 and module.params.get('tags'): - module.fail_json(msg='boto3 required when using tags for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg='region must be specified') - - try: - connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) - except (NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - - if module.params.get('tags'): - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - boto3_dynamodb = boto3_conn(module, conn_type='client', resource='dynamodb', region=region, endpoint=ec2_url, **aws_connect_kwargs) - if not hasattr(boto3_dynamodb, 'tag_resource'): - module.fail_json(msg='boto3 connection does not have tag_resource(), likely due to using an old version') - boto3_sts = boto3_conn(module, conn_type='client', resource='sts', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg='cannot connect to AWS', exception=traceback.format_exc()) - else: - boto3_dynamodb = None - boto3_sts = None - - state = module.params.get('state') - if state == 'present': - create_or_update_dynamo_table(connection, module, boto3_dynamodb, boto3_sts, region) - elif state == 'absent': - delete_dynamo_table(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/dynamodb_ttl.py b/lib/ansible/modules/cloud/amazon/dynamodb_ttl.py deleted file mode 100644 index 1096d9a4de..0000000000 --- a/lib/ansible/modules/cloud/amazon/dynamodb_ttl.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: dynamodb_ttl -short_description: Set TTL for a given DynamoDB table -description: -- Uses boto3 to set TTL. -- Requires botocore version 1.5.24 or higher. -version_added: "2.4" -options: - state: - description: - - State to set DynamoDB table to. - choices: ['enable', 'disable'] - required: false - type: str - table_name: - description: - - Name of the DynamoDB table to work on. - required: true - type: str - attribute_name: - description: - - The name of the Time To Live attribute used to store the expiration time for items in the table. - - This appears to be required by the API even when disabling TTL. - required: true - type: str - -author: Ted Timmons (@tedder) -extends_documentation_fragment: -- aws -- ec2 -requirements: [ botocore>=1.5.24, boto3 ] -''' - -EXAMPLES = ''' -- name: enable TTL on my cowfacts table - dynamodb_ttl: - state: enable - table_name: cowfacts - attribute_name: cow_deleted_date - -- name: disable TTL on my cowfacts table - dynamodb_ttl: - state: disable - table_name: cowfacts - attribute_name: cow_deleted_date -''' - -RETURN = ''' -current_status: - description: current or new TTL specification. - type: dict - returned: always - sample: - - { "AttributeName": "deploy_timestamp", "TimeToLiveStatus": "ENABLED" } - - { "AttributeName": "deploy_timestamp", "Enabled": true } -''' - -import distutils.version -import traceback - -try: - import botocore -except ImportError: - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, get_aws_connection_info - - -def get_current_ttl_state(c, table_name): - '''Fetch the state dict for a table.''' - current_state = c.describe_time_to_live(TableName=table_name) - return current_state.get('TimeToLiveDescription') - - -def does_state_need_changing(attribute_name, desired_state, current_spec): - '''Run checks to see if the table needs to be modified. Basically a dirty check.''' - if not current_spec: - # we don't have an entry (or a table?) - return True - - if desired_state.lower() == 'enable' and current_spec.get('TimeToLiveStatus') not in ['ENABLING', 'ENABLED']: - return True - if desired_state.lower() == 'disable' and current_spec.get('TimeToLiveStatus') not in ['DISABLING', 'DISABLED']: - return True - if attribute_name != current_spec.get('AttributeName'): - return True - - return False - - -def set_ttl_state(c, table_name, state, attribute_name): - '''Set our specification. Returns the update_time_to_live specification dict, - which is different than the describe_* call.''' - is_enabled = False - if state.lower() == 'enable': - is_enabled = True - - ret = c.update_time_to_live( - TableName=table_name, - TimeToLiveSpecification={ - 'Enabled': is_enabled, - 'AttributeName': attribute_name - } - ) - - return ret.get('TimeToLiveSpecification') - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(choices=['enable', 'disable']), - table_name=dict(required=True), - attribute_name=dict(required=True)) - ) - module = AnsibleModule( - argument_spec=argument_spec, - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - elif distutils.version.StrictVersion(botocore.__version__) < distutils.version.StrictVersion('1.5.24'): - # TTL was added in this version. - module.fail_json(msg='Found botocore in version {0}, but >= {1} is required for TTL support'.format(botocore.__version__, '1.5.24')) - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - dbclient = boto3_conn(module, conn_type='client', resource='dynamodb', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg=str(e)) - - result = {'changed': False} - state = module.params['state'] - - # wrap all our calls to catch the standard exceptions. We don't pass `module` in to the - # methods so it's easier to do here. - try: - current_state = get_current_ttl_state(dbclient, module.params['table_name']) - - if does_state_need_changing(module.params['attribute_name'], module.params['state'], current_state): - # changes needed - new_state = set_ttl_state(dbclient, module.params['table_name'], module.params['state'], module.params['attribute_name']) - result['current_status'] = new_state - result['changed'] = True - else: - # no changes needed - result['current_status'] = current_state - - except botocore.exceptions.ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.ParamValidationError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc()) - except ValueError as e: - module.fail_json(msg=str(e)) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py b/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py deleted file mode 100644 index fdb59659f7..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_ami_copy -short_description: copies AMI between AWS regions, return new image id -description: - - Copies AMI from a source region to a destination region. B(Since version 2.3 this module depends on boto3.) -version_added: "2.0" -options: - source_region: - description: - - The source region the AMI should be copied from. - required: true - type: str - source_image_id: - description: - - The ID of the AMI in source region that should be copied. - required: true - type: str - name: - description: - - The name of the new AMI to copy. (As of 2.3 the default is 'default', in prior versions it was 'null'.) - default: "default" - type: str - description: - description: - - An optional human-readable string describing the contents and purpose of the new AMI. - type: str - encrypted: - description: - - Whether or not the destination snapshots of the copied AMI should be encrypted. - version_added: "2.2" - type: bool - kms_key_id: - description: - - KMS key id used to encrypt the image. If not specified, uses default EBS Customer Master Key (CMK) for your account. - version_added: "2.2" - type: str - wait: - description: - - Wait for the copied AMI to be in state 'available' before returning. - type: bool - default: 'no' - wait_timeout: - description: - - How long before wait gives up, in seconds. Prior to 2.3 the default was 1200. - - From 2.3-2.5 this option was deprecated in favor of boto3 waiter defaults. - This was reenabled in 2.6 to allow timeouts greater than 10 minutes. - default: 600 - type: int - tags: - description: - - 'A hash/dictionary of tags to add to the new copied AMI: C({"key":"value"}) and C({"key":"value","key":"value"})' - type: dict - tag_equality: - description: - - Whether to use tags if the source AMI already exists in the target region. If this is set, and all tags match - in an existing AMI, the AMI will not be copied again. - default: false - type: bool - version_added: 2.6 -author: -- Amir Moulavi (@amir343) <amir.moulavi@gmail.com> -- Tim C (@defunctio) <defunct@defunct.io> -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 -''' - -EXAMPLES = ''' -# Basic AMI Copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - -# AMI copy wait until available -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - wait: yes - wait_timeout: 1200 # Default timeout is 600 - register: image_id - -# Named AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - name: My-Awesome-AMI - description: latest patch - -# Tagged AMI copy (will not copy the same AMI twice) -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - tags: - Name: My-Super-AMI - Patch: 1.2.3 - tag_equality: yes - -# Encrypted AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - -# Encrypted AMI copy with specified key -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b -''' - -RETURN = ''' -image_id: - description: AMI ID of the copied AMI - returned: always - type: str - sample: ami-e689729e -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list -from ansible.module_utils._text import to_native - -try: - from botocore.exceptions import ClientError, NoCredentialsError, WaiterError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - - -def copy_image(module, ec2): - """ - Copies an AMI - - module : AnsibleModule object - ec2: ec2 connection object - """ - - image = None - changed = False - tags = module.params.get('tags') - - params = {'SourceRegion': module.params.get('source_region'), - 'SourceImageId': module.params.get('source_image_id'), - 'Name': module.params.get('name'), - 'Description': module.params.get('description'), - 'Encrypted': module.params.get('encrypted'), - } - 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 = ec2.describe_images(Filters=filters) - if len(images['Images']) > 0: - image = images['Images'][0] - if not image: - image = ec2.copy_image(**params) - image_id = image['ImageId'] - if tags: - 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') - 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_aws(e, msg='An error occurred waiting for the image to become available') - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not copy AMI") - except Exception as e: - module.fail_json(msg='Unhandled exception. (%s)' % to_native(e)) - - -def main(): - argument_spec = dict( - source_region=dict(required=True), - source_image_id=dict(required=True), - name=dict(default='default'), - description=dict(default=''), - encrypted=dict(type='bool', default=False, required=False), - kms_key_id=dict(type='str', required=False), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=600), - tags=dict(type='dict'), - tag_equality=dict(type='bool', default=False)) - - module = AnsibleAWSModule(argument_spec=argument_spec) - # TODO: Check botocore version - ec2 = module.client('ec2') - copy_image(module, ec2) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg.py b/lib/ansible/modules/cloud/amazon/ec2_asg.py deleted file mode 100644 index 9c72e11d52..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_asg.py +++ /dev/null @@ -1,1831 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_asg -short_description: Create or delete AWS AutoScaling Groups (ASGs) -description: - - Can create or delete AWS AutoScaling Groups. - - Can be used with the M(ec2_lc) module to manage Launch Configurations. -version_added: "1.6" -author: "Gareth Rushgrove (@garethr)" -requirements: [ "boto3", "botocore" ] -options: - state: - description: - - Register or deregister the instance. - choices: ['present', 'absent'] - default: present - type: str - name: - description: - - Unique name for group to be created or deleted. - required: true - type: str - load_balancers: - description: - - List of ELB names to use for the group. Use for classic load balancers. - type: list - elements: str - target_group_arns: - description: - - List of target group ARNs to use for the group. Use for application load balancers. - version_added: "2.4" - type: list - elements: str - availability_zones: - description: - - List of availability zone names in which to create the group. - - Defaults to all the availability zones in the region if I(vpc_zone_identifier) is not set. - type: list - elements: str - launch_config_name: - description: - - Name of the Launch configuration to use for the group. See the M(ec2_lc) module for managing these. - - If unspecified then the current group value will be used. One of I(launch_config_name) or I(launch_template) must be provided. - type: str - launch_template: - description: - - Dictionary describing the Launch Template to use - suboptions: - version: - description: - - The version number of the launch template to use. - - Defaults to latest version if not provided. - type: str - launch_template_name: - description: - - The name of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required. - type: str - launch_template_id: - description: - - The id of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required. - type: str - type: dict - version_added: "2.8" - min_size: - description: - - Minimum number of instances in group, if unspecified then the current group value will be used. - type: int - max_size: - description: - - Maximum number of instances in group, if unspecified then the current group value will be used. - type: int - max_instance_lifetime: - description: - - The maximum amount of time, in seconds, that an instance can be in service. - - Maximum instance lifetime must be equal to 0, between 604800 and 31536000 seconds (inclusive), or not specified. - - Value of 0 removes lifetime restriction. - version_added: "2.10" - type: int - mixed_instances_policy: - description: - - A mixed instance policy to use for the ASG. - - Only used when the ASG is configured to use a Launch Template (I(launch_template)). - - 'See also U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-mixedinstancespolicy.html)' - required: false - version_added: "2.10" - suboptions: - instance_types: - description: - - A list of instance_types. - type: list - elements: str - type: dict - placement_group: - description: - - Physical location of your cluster placement group created in Amazon EC2. - version_added: "2.3" - type: str - desired_capacity: - description: - - Desired number of instances in group, if unspecified then the current group value will be used. - type: int - replace_all_instances: - description: - - In a rolling fashion, replace all instances that used the old launch configuration with one from the new launch configuration. - It increases the ASG size by I(replace_batch_size), waits for the new instances to be up and running. - After that, it terminates a batch of old instances, waits for the replacements, and repeats, until all old instances are replaced. - Once that's done the ASG size is reduced back to the expected size. - version_added: "1.8" - default: false - type: bool - replace_batch_size: - description: - - Number of instances you'd like to replace at a time. Used with I(replace_all_instances). - required: false - version_added: "1.8" - default: 1 - type: int - replace_instances: - description: - - List of I(instance_ids) belonging to the named AutoScalingGroup that you would like to terminate and be replaced with instances - matching the current launch configuration. - version_added: "1.8" - type: list - elements: str - lc_check: - description: - - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current I(launch_config). - version_added: "1.8" - default: true - type: bool - lt_check: - description: - - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current - I(launch_template or I(launch_template) I(version). - version_added: "2.8" - default: true - type: bool - vpc_zone_identifier: - description: - - List of VPC subnets to use - type: list - elements: str - tags: - description: - - A list of tags to add to the Auto Scale Group. - - Optional key is I(propagate_at_launch), which defaults to true. - - When I(propagate_at_launch) is true the tags will be propagated to the Instances created. - version_added: "1.7" - type: list - elements: dict - health_check_period: - description: - - Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. - required: false - default: 300 - version_added: "1.7" - type: int - health_check_type: - description: - - The service you want the health status from, Amazon EC2 or Elastic Load Balancer. - required: false - default: EC2 - version_added: "1.7" - choices: ['EC2', 'ELB'] - type: str - default_cooldown: - description: - - The number of seconds after a scaling activity completes before another can begin. - default: 300 - version_added: "2.0" - type: int - wait_timeout: - description: - - How long to wait for instances to become viable when replaced. If you experience the error "Waited too long for ELB instances to be healthy", - try increasing this value. - default: 300 - type: int - version_added: "1.8" - wait_for_instances: - description: - - Wait for the ASG instances to be in a ready state before exiting. If instances are behind an ELB, it will wait until the ELB determines all - instances have a lifecycle_state of "InService" and a health_status of "Healthy". - version_added: "1.9" - default: true - type: bool - termination_policies: - description: - - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity. - - Using I(termination_policies=Default) when modifying an existing AutoScalingGroup will result in the existing policy being retained - instead of changed to C(Default). - - 'Valid values include: C(Default), C(OldestInstance), C(NewestInstance), C(OldestLaunchConfiguration), C(ClosestToNextInstanceHour)' - - 'Full documentation of valid values can be found in the AWS documentation:' - - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html#custom-termination-policy)' - default: Default - version_added: "2.0" - type: list - elements: str - notification_topic: - description: - - A SNS topic ARN to send auto scaling notifications to. - version_added: "2.2" - type: str - notification_types: - description: - - A list of auto scaling events to trigger notifications on. - default: - - 'autoscaling:EC2_INSTANCE_LAUNCH' - - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' - - 'autoscaling:EC2_INSTANCE_TERMINATE' - - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' - required: false - version_added: "2.2" - type: list - elements: str - suspend_processes: - description: - - A list of scaling processes to suspend. - - 'Valid values include:' - - C(Launch), C(Terminate), C(HealthCheck), C(ReplaceUnhealthy), C(AZRebalance), C(AlarmNotification), C(ScheduledActions), C(AddToLoadBalancer) - - 'Full documentation of valid values can be found in the AWS documentation:' - - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html)' - default: [] - version_added: "2.3" - type: list - elements: str - metrics_collection: - description: - - Enable ASG metrics collection. - type: bool - default: false - version_added: "2.6" - metrics_granularity: - description: - - When I(metrics_collection=true) this will determine the granularity of metrics collected by CloudWatch. - default: "1Minute" - version_added: "2.6" - type: str - metrics_list: - description: - - List of autoscaling metrics to collect when I(metrics_collection=true). - default: - - 'GroupMinSize' - - 'GroupMaxSize' - - 'GroupDesiredCapacity' - - 'GroupInServiceInstances' - - 'GroupPendingInstances' - - 'GroupStandbyInstances' - - 'GroupTerminatingInstances' - - 'GroupTotalInstances' - version_added: "2.6" - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = ''' -# Basic configuration with Launch Configuration - -- ec2_asg: - name: special - load_balancers: [ 'lb1', 'lb2' ] - availability_zones: [ 'eu-west-1a', 'eu-west-1b' ] - launch_config_name: 'lc-1' - min_size: 1 - max_size: 10 - desired_capacity: 5 - vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] - tags: - - environment: production - propagate_at_launch: no - -# Rolling ASG Updates - -# Below is an example of how to assign a new launch config to an ASG and terminate old instances. -# -# All instances in "myasg" that do not have the launch configuration named "my_new_lc" will be terminated in -# a rolling fashion with instances using the current launch configuration, "my_new_lc". -# -# This could also be considered a rolling deploy of a pre-baked AMI. -# -# If this is a newly created group, the instances will not be replaced since all instances -# will have the current launch configuration. - -- name: create launch config - ec2_lc: - name: my_new_lc - image_id: ami-lkajsf - key_name: mykey - region: us-east-1 - security_groups: sg-23423 - instance_type: m1.small - assign_public_ip: yes - -- ec2_asg: - name: myasg - launch_config_name: my_new_lc - health_check_period: 60 - health_check_type: ELB - replace_all_instances: yes - min_size: 5 - max_size: 5 - desired_capacity: 5 - region: us-east-1 - -# To only replace a couple of instances instead of all of them, supply a list -# to "replace_instances": - -- ec2_asg: - name: myasg - launch_config_name: my_new_lc - health_check_period: 60 - health_check_type: ELB - replace_instances: - - i-b345231 - - i-24c2931 - min_size: 5 - max_size: 5 - desired_capacity: 5 - region: us-east-1 - -# Basic Configuration with Launch Template - -- ec2_asg: - name: special - load_balancers: [ 'lb1', 'lb2' ] - availability_zones: [ 'eu-west-1a', 'eu-west-1b' ] - launch_template: - version: '1' - launch_template_name: 'lt-example' - launch_template_id: 'lt-123456' - min_size: 1 - max_size: 10 - desired_capacity: 5 - vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] - tags: - - environment: production - propagate_at_launch: no - -# Basic Configuration with Launch Template using mixed instance policy - -- ec2_asg: - name: special - load_balancers: [ 'lb1', 'lb2' ] - availability_zones: [ 'eu-west-1a', 'eu-west-1b' ] - launch_template: - version: '1' - launch_template_name: 'lt-example' - launch_template_id: 'lt-123456' - mixed_instances_policy: - instance_types: - - t3a.large - - t3.large - - t2.large - min_size: 1 - max_size: 10 - desired_capacity: 5 - vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] - tags: - - environment: production - propagate_at_launch: no -''' - -RETURN = ''' ---- -auto_scaling_group_name: - description: The unique name of the auto scaling group - returned: success - type: str - sample: "myasg" -auto_scaling_group_arn: - description: The unique ARN of the autoscaling group - returned: success - type: str - sample: "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:6a09ad6d-eeee-1234-b987-ee123ced01ad:autoScalingGroupName/myasg" -availability_zones: - description: The availability zones for the auto scaling group - returned: success - type: list - sample: [ - "us-east-1d" - ] -created_time: - description: Timestamp of create time of the auto scaling group - returned: success - type: str - sample: "2017-11-08T14:41:48.272000+00:00" -default_cooldown: - description: The default cooldown time in seconds. - returned: success - type: int - sample: 300 -desired_capacity: - description: The number of EC2 instances that should be running in this group. - returned: success - type: int - sample: 3 -healthcheck_period: - description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. - returned: success - type: int - sample: 30 -healthcheck_type: - description: The service you want the health status from, one of "EC2" or "ELB". - returned: success - type: str - sample: "ELB" -healthy_instances: - description: Number of instances in a healthy state - returned: success - type: int - sample: 5 -in_service_instances: - description: Number of instances in service - returned: success - type: int - sample: 3 -instance_facts: - description: Dictionary of EC2 instances and their status as it relates to the ASG. - returned: success - type: dict - sample: { - "i-0123456789012": { - "health_status": "Healthy", - "launch_config_name": "public-webapp-production-1", - "lifecycle_state": "InService" - } - } -instances: - description: list of instance IDs in the ASG - returned: success - type: list - sample: [ - "i-0123456789012" - ] -launch_config_name: - description: > - Name of launch configuration associated with the ASG. Same as launch_configuration_name, - provided for compatibility with ec2_asg module. - returned: success - type: str - sample: "public-webapp-production-1" -load_balancers: - description: List of load balancers names attached to the ASG. - returned: success - type: list - sample: ["elb-webapp-prod"] -max_instance_lifetime: - description: The maximum amount of time, in seconds, that an instance can be in service. - returned: success - type: int - sample: 604800 -max_size: - description: Maximum size of group - returned: success - type: int - sample: 3 -min_size: - description: Minimum size of group - returned: success - type: int - sample: 1 -mixed_instance_policy: - description: Returns the list of instance types if a mixed instance policy is set. - returned: success - type: list - sample: ["t3.micro", "t3a.micro"] -pending_instances: - description: Number of instances in pending state - returned: success - type: int - sample: 1 -tags: - description: List of tags for the ASG, and whether or not each tag propagates to instances at launch. - returned: success - type: list - sample: [ - { - "key": "Name", - "value": "public-webapp-production-1", - "resource_id": "public-webapp-production-1", - "resource_type": "auto-scaling-group", - "propagate_at_launch": "true" - }, - { - "key": "env", - "value": "production", - "resource_id": "public-webapp-production-1", - "resource_type": "auto-scaling-group", - "propagate_at_launch": "true" - } - ] -target_group_arns: - description: List of ARNs of the target groups that the ASG populates - returned: success - type: list - sample: [ - "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b", - "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234" - ] -target_group_names: - description: List of names of the target groups that the ASG populates - returned: success - type: list - sample: [ - "target-group-host-hello", - "target-group-path-world" - ] -termination_policies: - description: A list of termination policies for the group. - returned: success - type: list - sample: ["Default"] -unhealthy_instances: - description: Number of instances in an unhealthy state - returned: success - type: int - sample: 0 -viable_instances: - description: Number of instances in a viable state - returned: success - type: int - sample: 1 -vpc_zone_identifier: - description: VPC zone ID / subnet id for the auto scaling group - returned: success - type: str - sample: "subnet-a31ef45f" -metrics_collection: - description: List of enabled AutosSalingGroup metrics - returned: success - type: list - sample: [ - { - "Granularity": "1Minute", - "Metric": "GroupInServiceInstances" - } - ] -''' - -import time -import traceback - -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ( - AWSRetry, - camel_dict_to_snake_dict -) - -try: - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - -from ansible.module_utils.aws.core import AnsibleAWSModule - -ASG_ATTRIBUTES = ('AvailabilityZones', 'DefaultCooldown', 'DesiredCapacity', - 'HealthCheckGracePeriod', 'HealthCheckType', 'LaunchConfigurationName', - 'LoadBalancerNames', 'MaxInstanceLifetime', 'MaxSize', 'MinSize', - 'AutoScalingGroupName', 'PlacementGroup', 'TerminationPolicies', - 'VPCZoneIdentifier') - -INSTANCE_ATTRIBUTES = ('instance_id', 'health_status', 'lifecycle_state', 'launch_config_name') - -backoff_params = dict(tries=10, delay=3, backoff=1.5) - - -@AWSRetry.backoff(**backoff_params) -def describe_autoscaling_groups(connection, group_name): - pg = connection.get_paginator('describe_auto_scaling_groups') - return pg.paginate(AutoScalingGroupNames=[group_name]).build_full_result().get('AutoScalingGroups', []) - - -@AWSRetry.backoff(**backoff_params) -def deregister_lb_instances(connection, lb_name, instance_id): - connection.deregister_instances_from_load_balancer(LoadBalancerName=lb_name, Instances=[dict(InstanceId=instance_id)]) - - -@AWSRetry.backoff(**backoff_params) -def describe_instance_health(connection, lb_name, instances): - params = dict(LoadBalancerName=lb_name) - if instances: - params.update(Instances=instances) - return connection.describe_instance_health(**params) - - -@AWSRetry.backoff(**backoff_params) -def describe_target_health(connection, target_group_arn, instances): - return connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=instances) - - -@AWSRetry.backoff(**backoff_params) -def suspend_asg_processes(connection, asg_name, processes): - connection.suspend_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) - - -@AWSRetry.backoff(**backoff_params) -def resume_asg_processes(connection, asg_name, processes): - connection.resume_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) - - -@AWSRetry.backoff(**backoff_params) -def describe_launch_configurations(connection, launch_config_name): - pg = connection.get_paginator('describe_launch_configurations') - return pg.paginate(LaunchConfigurationNames=[launch_config_name]).build_full_result() - - -@AWSRetry.backoff(**backoff_params) -def describe_launch_templates(connection, launch_template): - if launch_template['launch_template_id'] is not None: - try: - lt = connection.describe_launch_templates(LaunchTemplateIds=[launch_template['launch_template_id']]) - return lt - except (botocore.exceptions.ClientError) as e: - module.fail_json(msg="No launch template found matching: %s" % launch_template) - else: - try: - lt = connection.describe_launch_templates(LaunchTemplateNames=[launch_template['launch_template_name']]) - return lt - except (botocore.exceptions.ClientError) as e: - module.fail_json(msg="No launch template found matching: %s" % launch_template) - - -@AWSRetry.backoff(**backoff_params) -def create_asg(connection, **params): - connection.create_auto_scaling_group(**params) - - -@AWSRetry.backoff(**backoff_params) -def put_notification_config(connection, asg_name, topic_arn, notification_types): - connection.put_notification_configuration( - AutoScalingGroupName=asg_name, - TopicARN=topic_arn, - NotificationTypes=notification_types - ) - - -@AWSRetry.backoff(**backoff_params) -def del_notification_config(connection, asg_name, topic_arn): - connection.delete_notification_configuration( - AutoScalingGroupName=asg_name, - TopicARN=topic_arn - ) - - -@AWSRetry.backoff(**backoff_params) -def attach_load_balancers(connection, asg_name, load_balancers): - connection.attach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) - - -@AWSRetry.backoff(**backoff_params) -def detach_load_balancers(connection, asg_name, load_balancers): - connection.detach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) - - -@AWSRetry.backoff(**backoff_params) -def attach_lb_target_groups(connection, asg_name, target_group_arns): - connection.attach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) - - -@AWSRetry.backoff(**backoff_params) -def detach_lb_target_groups(connection, asg_name, target_group_arns): - connection.detach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) - - -@AWSRetry.backoff(**backoff_params) -def update_asg(connection, **params): - connection.update_auto_scaling_group(**params) - - -@AWSRetry.backoff(catch_extra_error_codes=['ScalingActivityInProgress'], **backoff_params) -def delete_asg(connection, asg_name, force_delete): - connection.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=force_delete) - - -@AWSRetry.backoff(**backoff_params) -def terminate_asg_instance(connection, instance_id, decrement_capacity): - connection.terminate_instance_in_auto_scaling_group(InstanceId=instance_id, - ShouldDecrementDesiredCapacity=decrement_capacity) - - -def enforce_required_arguments_for_create(): - ''' As many arguments are not required for autoscale group deletion - they cannot be mandatory arguments for the module, so we enforce - them here ''' - missing_args = [] - if module.params.get('launch_config_name') is None and module.params.get('launch_template') is None: - module.fail_json(msg="Missing either launch_config_name or launch_template for autoscaling group create") - for arg in ('min_size', 'max_size'): - if module.params[arg] is None: - missing_args.append(arg) - if missing_args: - module.fail_json(msg="Missing required arguments for autoscaling group create: %s" % ",".join(missing_args)) - - -def get_properties(autoscaling_group): - properties = dict( - healthy_instances=0, - in_service_instances=0, - unhealthy_instances=0, - pending_instances=0, - viable_instances=0, - terminating_instances=0 - ) - instance_facts = dict() - autoscaling_group_instances = autoscaling_group.get('Instances') - - if autoscaling_group_instances: - properties['instances'] = [i['InstanceId'] for i in autoscaling_group_instances] - for i in autoscaling_group_instances: - instance_facts[i['InstanceId']] = { - 'health_status': i['HealthStatus'], - 'lifecycle_state': i['LifecycleState'] - } - if 'LaunchConfigurationName' in i: - instance_facts[i['InstanceId']]['launch_config_name'] = i['LaunchConfigurationName'] - elif 'LaunchTemplate' in i: - instance_facts[i['InstanceId']]['launch_template'] = i['LaunchTemplate'] - - if i['HealthStatus'] == 'Healthy' and i['LifecycleState'] == 'InService': - properties['viable_instances'] += 1 - - if i['HealthStatus'] == 'Healthy': - properties['healthy_instances'] += 1 - else: - properties['unhealthy_instances'] += 1 - - if i['LifecycleState'] == 'InService': - properties['in_service_instances'] += 1 - if i['LifecycleState'] == 'Terminating': - properties['terminating_instances'] += 1 - if i['LifecycleState'] == 'Pending': - properties['pending_instances'] += 1 - else: - properties['instances'] = [] - - properties['auto_scaling_group_name'] = autoscaling_group.get('AutoScalingGroupName') - properties['auto_scaling_group_arn'] = autoscaling_group.get('AutoScalingGroupARN') - properties['availability_zones'] = autoscaling_group.get('AvailabilityZones') - properties['created_time'] = autoscaling_group.get('CreatedTime') - properties['instance_facts'] = instance_facts - properties['load_balancers'] = autoscaling_group.get('LoadBalancerNames') - if 'LaunchConfigurationName' in autoscaling_group: - properties['launch_config_name'] = autoscaling_group.get('LaunchConfigurationName') - else: - properties['launch_template'] = autoscaling_group.get('LaunchTemplate') - properties['tags'] = autoscaling_group.get('Tags') - properties['max_instance_lifetime'] = autoscaling_group.get('MaxInstanceLifetime') - properties['min_size'] = autoscaling_group.get('MinSize') - properties['max_size'] = autoscaling_group.get('MaxSize') - properties['desired_capacity'] = autoscaling_group.get('DesiredCapacity') - properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown') - properties['healthcheck_grace_period'] = autoscaling_group.get('HealthCheckGracePeriod') - properties['healthcheck_type'] = autoscaling_group.get('HealthCheckType') - properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown') - properties['termination_policies'] = autoscaling_group.get('TerminationPolicies') - properties['target_group_arns'] = autoscaling_group.get('TargetGroupARNs') - properties['vpc_zone_identifier'] = autoscaling_group.get('VPCZoneIdentifier') - raw_mixed_instance_object = autoscaling_group.get('MixedInstancesPolicy') - if raw_mixed_instance_object: - properties['mixed_instances_policy'] = [x['InstanceType'] for x in raw_mixed_instance_object.get('LaunchTemplate').get('Overrides')] - - metrics = autoscaling_group.get('EnabledMetrics') - if metrics: - metrics.sort(key=lambda x: x["Metric"]) - properties['metrics_collection'] = metrics - - if properties['target_group_arns']: - elbv2_connection = module.client('elbv2') - tg_paginator = elbv2_connection.get_paginator('describe_target_groups') - tg_result = tg_paginator.paginate( - TargetGroupArns=properties['target_group_arns'] - ).build_full_result() - target_groups = tg_result['TargetGroups'] - else: - target_groups = [] - - properties['target_group_names'] = [ - tg['TargetGroupName'] - for tg in target_groups - ] - - return properties - - -def get_launch_object(connection, ec2_connection): - launch_object = dict() - launch_config_name = module.params.get('launch_config_name') - launch_template = module.params.get('launch_template') - mixed_instances_policy = module.params.get('mixed_instances_policy') - if launch_config_name is None and launch_template is None: - return launch_object - elif launch_config_name: - try: - launch_configs = describe_launch_configurations(connection, launch_config_name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to describe launch configurations", - exception=traceback.format_exc()) - if len(launch_configs['LaunchConfigurations']) == 0: - module.fail_json(msg="No launch config found with name %s" % launch_config_name) - launch_object = {"LaunchConfigurationName": launch_configs['LaunchConfigurations'][0]['LaunchConfigurationName']} - return launch_object - elif launch_template: - lt = describe_launch_templates(ec2_connection, launch_template)['LaunchTemplates'][0] - if launch_template['version'] is not None: - launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": launch_template['version']}} - else: - launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": str(lt['LatestVersionNumber'])}} - - if mixed_instances_policy: - instance_types = mixed_instances_policy.get('instance_types', []) - policy = { - 'LaunchTemplate': { - 'LaunchTemplateSpecification': launch_object['LaunchTemplate'] - } - } - if instance_types: - policy['LaunchTemplate']['Overrides'] = [] - for instance_type in instance_types: - instance_type_dict = {'InstanceType': instance_type} - policy['LaunchTemplate']['Overrides'].append(instance_type_dict) - launch_object['MixedInstancesPolicy'] = policy - return launch_object - - -def elb_dreg(asg_connection, group_name, instance_id): - as_group = describe_autoscaling_groups(asg_connection, group_name)[0] - wait_timeout = module.params.get('wait_timeout') - count = 1 - if as_group['LoadBalancerNames'] and as_group['HealthCheckType'] == 'ELB': - elb_connection = module.client('elb') - else: - return - - for lb in as_group['LoadBalancerNames']: - deregister_lb_instances(elb_connection, lb, instance_id) - module.debug("De-registering %s from ELB %s" % (instance_id, lb)) - - wait_timeout = time.time() + wait_timeout - while wait_timeout > time.time() and count > 0: - count = 0 - for lb in as_group['LoadBalancerNames']: - lb_instances = describe_instance_health(elb_connection, lb, []) - for i in lb_instances['InstanceStates']: - if i['InstanceId'] == instance_id and i['State'] == "InService": - count += 1 - module.debug("%s: %s, %s" % (i['InstanceId'], i['State'], i['Description'])) - time.sleep(10) - - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for instance to deregister. {0}".format(time.asctime())) - - -def elb_healthy(asg_connection, elb_connection, group_name): - healthy_instances = set() - as_group = describe_autoscaling_groups(asg_connection, group_name)[0] - props = get_properties(as_group) - # get healthy, inservice instances from ASG - instances = [] - for instance, settings in props['instance_facts'].items(): - if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy': - instances.append(dict(InstanceId=instance)) - module.debug("ASG considers the following instances InService and Healthy: %s" % instances) - module.debug("ELB instance status:") - lb_instances = list() - for lb in as_group.get('LoadBalancerNames'): - # we catch a race condition that sometimes happens if the instance exists in the ASG - # but has not yet show up in the ELB - try: - lb_instances = describe_instance_health(elb_connection, lb, instances) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidInstance': - return None - - module.fail_json(msg="Failed to get load balancer.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to get load balancer.", - exception=traceback.format_exc()) - - for i in lb_instances.get('InstanceStates'): - if i['State'] == "InService": - healthy_instances.add(i['InstanceId']) - module.debug("ELB Health State %s: %s" % (i['InstanceId'], i['State'])) - return len(healthy_instances) - - -def tg_healthy(asg_connection, elbv2_connection, group_name): - healthy_instances = set() - as_group = describe_autoscaling_groups(asg_connection, group_name)[0] - props = get_properties(as_group) - # get healthy, inservice instances from ASG - instances = [] - for instance, settings in props['instance_facts'].items(): - if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy': - instances.append(dict(Id=instance)) - module.debug("ASG considers the following instances InService and Healthy: %s" % instances) - module.debug("Target Group instance status:") - tg_instances = list() - for tg in as_group.get('TargetGroupARNs'): - # we catch a race condition that sometimes happens if the instance exists in the ASG - # but has not yet show up in the ELB - try: - tg_instances = describe_target_health(elbv2_connection, tg, instances) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidInstance': - return None - - module.fail_json(msg="Failed to get target group.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to get target group.", - exception=traceback.format_exc()) - - for i in tg_instances.get('TargetHealthDescriptions'): - if i['TargetHealth']['State'] == "healthy": - healthy_instances.add(i['Target']['Id']) - module.debug("Target Group Health State %s: %s" % (i['Target']['Id'], i['TargetHealth']['State'])) - return len(healthy_instances) - - -def wait_for_elb(asg_connection, group_name): - wait_timeout = module.params.get('wait_timeout') - - # if the health_check_type is ELB, we want to query the ELBs directly for instance - # status as to avoid health_check_grace period that is awarded to ASG instances - as_group = describe_autoscaling_groups(asg_connection, group_name)[0] - - if as_group.get('LoadBalancerNames') and as_group.get('HealthCheckType') == 'ELB': - module.debug("Waiting for ELB to consider instances healthy.") - elb_connection = module.client('elb') - - wait_timeout = time.time() + wait_timeout - healthy_instances = elb_healthy(asg_connection, elb_connection, group_name) - - while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time(): - healthy_instances = elb_healthy(asg_connection, elb_connection, group_name) - module.debug("ELB thinks %s instances are healthy." % healthy_instances) - time.sleep(10) - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime()) - module.debug("Waiting complete. ELB thinks %s instances are healthy." % healthy_instances) - - -def wait_for_target_group(asg_connection, group_name): - wait_timeout = module.params.get('wait_timeout') - - # if the health_check_type is ELB, we want to query the ELBs directly for instance - # status as to avoid health_check_grace period that is awarded to ASG instances - as_group = describe_autoscaling_groups(asg_connection, group_name)[0] - - if as_group.get('TargetGroupARNs') and as_group.get('HealthCheckType') == 'ELB': - module.debug("Waiting for Target Group to consider instances healthy.") - elbv2_connection = module.client('elbv2') - - wait_timeout = time.time() + wait_timeout - healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name) - - while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time(): - healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name) - module.debug("Target Group thinks %s instances are healthy." % healthy_instances) - time.sleep(10) - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime()) - module.debug("Waiting complete. Target Group thinks %s instances are healthy." % healthy_instances) - - -def suspend_processes(ec2_connection, as_group): - suspend_processes = set(module.params.get('suspend_processes')) - - try: - suspended_processes = set([p['ProcessName'] for p in as_group['SuspendedProcesses']]) - except AttributeError: - # New ASG being created, no suspended_processes defined yet - suspended_processes = set() - - if suspend_processes == suspended_processes: - return False - - resume_processes = list(suspended_processes - suspend_processes) - if resume_processes: - resume_asg_processes(ec2_connection, module.params.get('name'), resume_processes) - - if suspend_processes: - suspend_asg_processes(ec2_connection, module.params.get('name'), list(suspend_processes)) - - return True - - -def create_autoscaling_group(connection): - group_name = module.params.get('name') - load_balancers = module.params['load_balancers'] - target_group_arns = module.params['target_group_arns'] - availability_zones = module.params['availability_zones'] - launch_config_name = module.params.get('launch_config_name') - launch_template = module.params.get('launch_template') - mixed_instances_policy = module.params.get('mixed_instances_policy') - min_size = module.params['min_size'] - max_size = module.params['max_size'] - max_instance_lifetime = module.params.get('max_instance_lifetime') - placement_group = module.params.get('placement_group') - desired_capacity = module.params.get('desired_capacity') - vpc_zone_identifier = module.params.get('vpc_zone_identifier') - set_tags = module.params.get('tags') - health_check_period = module.params.get('health_check_period') - health_check_type = module.params.get('health_check_type') - default_cooldown = module.params.get('default_cooldown') - wait_for_instances = module.params.get('wait_for_instances') - wait_timeout = module.params.get('wait_timeout') - termination_policies = module.params.get('termination_policies') - notification_topic = module.params.get('notification_topic') - notification_types = module.params.get('notification_types') - metrics_collection = module.params.get('metrics_collection') - metrics_granularity = module.params.get('metrics_granularity') - metrics_list = module.params.get('metrics_list') - - try: - as_groups = describe_autoscaling_groups(connection, group_name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to describe auto scaling groups.", - exception=traceback.format_exc()) - - ec2_connection = module.client('ec2') - - if vpc_zone_identifier: - vpc_zone_identifier = ','.join(vpc_zone_identifier) - - asg_tags = [] - for tag in set_tags: - for k, v in tag.items(): - if k != 'propagate_at_launch': - asg_tags.append(dict(Key=k, - Value=to_native(v), - PropagateAtLaunch=bool(tag.get('propagate_at_launch', True)), - ResourceType='auto-scaling-group', - ResourceId=group_name)) - if not as_groups: - if not vpc_zone_identifier and not availability_zones: - availability_zones = module.params['availability_zones'] = [zone['ZoneName'] for - zone in ec2_connection.describe_availability_zones()['AvailabilityZones']] - - enforce_required_arguments_for_create() - - if desired_capacity is None: - desired_capacity = min_size - ag = dict( - AutoScalingGroupName=group_name, - MinSize=min_size, - MaxSize=max_size, - DesiredCapacity=desired_capacity, - Tags=asg_tags, - HealthCheckGracePeriod=health_check_period, - HealthCheckType=health_check_type, - DefaultCooldown=default_cooldown, - TerminationPolicies=termination_policies) - if vpc_zone_identifier: - ag['VPCZoneIdentifier'] = vpc_zone_identifier - if availability_zones: - ag['AvailabilityZones'] = availability_zones - if placement_group: - ag['PlacementGroup'] = placement_group - if load_balancers: - ag['LoadBalancerNames'] = load_balancers - if target_group_arns: - ag['TargetGroupARNs'] = target_group_arns - if max_instance_lifetime: - ag['MaxInstanceLifetime'] = max_instance_lifetime - - launch_object = get_launch_object(connection, ec2_connection) - if 'LaunchConfigurationName' in launch_object: - ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName'] - elif 'LaunchTemplate' in launch_object: - if 'MixedInstancesPolicy' in launch_object: - ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] - else: - ag['LaunchTemplate'] = launch_object['LaunchTemplate'] - else: - module.fail_json(msg="Missing LaunchConfigurationName or LaunchTemplate", - exception=traceback.format_exc()) - - try: - create_asg(connection, **ag) - if metrics_collection: - connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list) - - all_ag = describe_autoscaling_groups(connection, group_name) - if len(all_ag) == 0: - module.fail_json(msg="No auto scaling group found with the name %s" % group_name) - as_group = all_ag[0] - suspend_processes(connection, as_group) - if wait_for_instances: - wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances') - if load_balancers: - wait_for_elb(connection, group_name) - # Wait for target group health if target group(s)defined - if target_group_arns: - wait_for_target_group(connection, group_name) - if notification_topic: - put_notification_config(connection, group_name, notification_topic, notification_types) - as_group = describe_autoscaling_groups(connection, group_name)[0] - asg_properties = get_properties(as_group) - changed = True - return changed, asg_properties - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to create Autoscaling Group.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to create Autoscaling Group.", - exception=traceback.format_exc()) - else: - as_group = as_groups[0] - initial_asg_properties = get_properties(as_group) - changed = False - - if suspend_processes(connection, as_group): - changed = True - - # process tag changes - if len(set_tags) > 0: - have_tags = as_group.get('Tags') - want_tags = asg_tags - if have_tags: - have_tags.sort(key=lambda x: x["Key"]) - if want_tags: - want_tags.sort(key=lambda x: x["Key"]) - dead_tags = [] - have_tag_keyvals = [x['Key'] for x in have_tags] - want_tag_keyvals = [x['Key'] for x in want_tags] - - for dead_tag in set(have_tag_keyvals).difference(want_tag_keyvals): - changed = True - dead_tags.append(dict(ResourceId=as_group['AutoScalingGroupName'], - ResourceType='auto-scaling-group', Key=dead_tag)) - have_tags = [have_tag for have_tag in have_tags if have_tag['Key'] != dead_tag] - if dead_tags: - connection.delete_tags(Tags=dead_tags) - - zipped = zip(have_tags, want_tags) - if len(have_tags) != len(want_tags) or not all(x == y for x, y in zipped): - changed = True - connection.create_or_update_tags(Tags=asg_tags) - - # Handle load balancer attachments/detachments - # Attach load balancers if they are specified but none currently exist - if load_balancers and not as_group['LoadBalancerNames']: - changed = True - try: - attach_load_balancers(connection, group_name, load_balancers) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to update Autoscaling Group.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to update Autoscaling Group.", - exception=traceback.format_exc()) - - # Update load balancers if they are specified and one or more already exists - elif as_group['LoadBalancerNames']: - change_load_balancers = load_balancers is not None - # Get differences - if not load_balancers: - load_balancers = list() - wanted_elbs = set(load_balancers) - - has_elbs = set(as_group['LoadBalancerNames']) - # check if all requested are already existing - if has_elbs - wanted_elbs and change_load_balancers: - # if wanted contains less than existing, then we need to delete some - elbs_to_detach = has_elbs.difference(wanted_elbs) - if elbs_to_detach: - changed = True - try: - detach_load_balancers(connection, group_name, list(elbs_to_detach)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to detach load balancers %s: %s." % (elbs_to_detach, to_native(e)), - exception=traceback.format_exc()) - if wanted_elbs - has_elbs: - # if has contains less than wanted, then we need to add some - elbs_to_attach = wanted_elbs.difference(has_elbs) - if elbs_to_attach: - changed = True - try: - attach_load_balancers(connection, group_name, list(elbs_to_attach)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to attach load balancers %s: %s." % (elbs_to_attach, to_native(e)), - exception=traceback.format_exc()) - - # Handle target group attachments/detachments - # Attach target groups if they are specified but none currently exist - if target_group_arns and not as_group['TargetGroupARNs']: - changed = True - try: - attach_lb_target_groups(connection, group_name, target_group_arns) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to update Autoscaling Group.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to update Autoscaling Group.", - exception=traceback.format_exc()) - # Update target groups if they are specified and one or more already exists - elif target_group_arns is not None and as_group['TargetGroupARNs']: - # Get differences - wanted_tgs = set(target_group_arns) - has_tgs = set(as_group['TargetGroupARNs']) - # check if all requested are already existing - if has_tgs.issuperset(wanted_tgs): - # if wanted contains less than existing, then we need to delete some - tgs_to_detach = has_tgs.difference(wanted_tgs) - if tgs_to_detach: - changed = True - try: - detach_lb_target_groups(connection, group_name, list(tgs_to_detach)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to detach load balancer target groups %s: %s" % (tgs_to_detach, to_native(e)), - exception=traceback.format_exc()) - if wanted_tgs.issuperset(has_tgs): - # if has contains less than wanted, then we need to add some - tgs_to_attach = wanted_tgs.difference(has_tgs) - if tgs_to_attach: - changed = True - try: - attach_lb_target_groups(connection, group_name, list(tgs_to_attach)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to attach load balancer target groups %s: %s" % (tgs_to_attach, to_native(e)), - exception=traceback.format_exc()) - - # check for attributes that aren't required for updating an existing ASG - # check if min_size/max_size/desired capacity have been specified and if not use ASG values - if min_size is None: - min_size = as_group['MinSize'] - if max_size is None: - max_size = as_group['MaxSize'] - if desired_capacity is None: - desired_capacity = as_group['DesiredCapacity'] - ag = dict( - AutoScalingGroupName=group_name, - MinSize=min_size, - MaxSize=max_size, - DesiredCapacity=desired_capacity, - HealthCheckGracePeriod=health_check_period, - HealthCheckType=health_check_type, - DefaultCooldown=default_cooldown, - TerminationPolicies=termination_policies) - - # Get the launch object (config or template) if one is provided in args or use the existing one attached to ASG if not. - launch_object = get_launch_object(connection, ec2_connection) - if 'LaunchConfigurationName' in launch_object: - ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName'] - elif 'LaunchTemplate' in launch_object: - if 'MixedInstancesPolicy' in launch_object: - ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] - else: - ag['LaunchTemplate'] = launch_object['LaunchTemplate'] - else: - try: - ag['LaunchConfigurationName'] = as_group['LaunchConfigurationName'] - except Exception: - launch_template = as_group['LaunchTemplate'] - # Prefer LaunchTemplateId over Name as it's more specific. Only one can be used for update_asg. - ag['LaunchTemplate'] = {"LaunchTemplateId": launch_template['LaunchTemplateId'], "Version": launch_template['Version']} - - if availability_zones: - ag['AvailabilityZones'] = availability_zones - if vpc_zone_identifier: - ag['VPCZoneIdentifier'] = vpc_zone_identifier - if max_instance_lifetime is not None: - ag['MaxInstanceLifetime'] = max_instance_lifetime - - try: - update_asg(connection, **ag) - - if metrics_collection: - connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list) - else: - connection.disable_metrics_collection(AutoScalingGroupName=group_name, Metrics=metrics_list) - - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to update autoscaling group: %s" % to_native(e), - exception=traceback.format_exc()) - if notification_topic: - try: - put_notification_config(connection, group_name, notification_topic, notification_types) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to update Autoscaling Group notifications.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to update Autoscaling Group notifications.", - exception=traceback.format_exc()) - if wait_for_instances: - wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances') - # Wait for ELB health if ELB(s)defined - if load_balancers: - module.debug('\tWAITING FOR ELB HEALTH') - wait_for_elb(connection, group_name) - # Wait for target group health if target group(s)defined - - if target_group_arns: - module.debug('\tWAITING FOR TG HEALTH') - wait_for_target_group(connection, group_name) - - try: - as_group = describe_autoscaling_groups(connection, group_name)[0] - asg_properties = get_properties(as_group) - if asg_properties != initial_asg_properties: - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to read existing Autoscaling Groups.", - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Failed to read existing Autoscaling Groups.", - exception=traceback.format_exc()) - return changed, asg_properties - - -def delete_autoscaling_group(connection): - group_name = module.params.get('name') - notification_topic = module.params.get('notification_topic') - wait_for_instances = module.params.get('wait_for_instances') - wait_timeout = module.params.get('wait_timeout') - - if notification_topic: - del_notification_config(connection, group_name, notification_topic) - groups = describe_autoscaling_groups(connection, group_name) - if groups: - wait_timeout = time.time() + wait_timeout - if not wait_for_instances: - delete_asg(connection, group_name, force_delete=True) - else: - updated_params = dict(AutoScalingGroupName=group_name, MinSize=0, MaxSize=0, DesiredCapacity=0) - update_asg(connection, **updated_params) - instances = True - while instances and wait_for_instances and wait_timeout >= time.time(): - tmp_groups = describe_autoscaling_groups(connection, group_name) - if tmp_groups: - tmp_group = tmp_groups[0] - if not tmp_group.get('Instances'): - instances = False - time.sleep(10) - - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime()) - - delete_asg(connection, group_name, force_delete=False) - while describe_autoscaling_groups(connection, group_name) and wait_timeout >= time.time(): - time.sleep(5) - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for ASG to delete. %s" % time.asctime()) - return True - - return False - - -def get_chunks(l, n): - for i in range(0, len(l), n): - yield l[i:i + n] - - -def update_size(connection, group, max_size, min_size, dc): - module.debug("setting ASG sizes") - module.debug("minimum size: %s, desired_capacity: %s, max size: %s" % (min_size, dc, max_size)) - updated_group = dict() - updated_group['AutoScalingGroupName'] = group['AutoScalingGroupName'] - updated_group['MinSize'] = min_size - updated_group['MaxSize'] = max_size - updated_group['DesiredCapacity'] = dc - update_asg(connection, **updated_group) - - -def replace(connection): - batch_size = module.params.get('replace_batch_size') - wait_timeout = module.params.get('wait_timeout') - wait_for_instances = module.params.get('wait_for_instances') - group_name = module.params.get('name') - max_size = module.params.get('max_size') - min_size = module.params.get('min_size') - desired_capacity = module.params.get('desired_capacity') - launch_config_name = module.params.get('launch_config_name') - # Required to maintain the default value being set to 'true' - if launch_config_name: - lc_check = module.params.get('lc_check') - else: - lc_check = False - # Mirror above behavior for Launch Templates - launch_template = module.params.get('launch_template') - if launch_template: - lt_check = module.params.get('lt_check') - else: - lt_check = False - replace_instances = module.params.get('replace_instances') - replace_all_instances = module.params.get('replace_all_instances') - - as_group = describe_autoscaling_groups(connection, group_name)[0] - if desired_capacity is None: - desired_capacity = as_group['DesiredCapacity'] - - if wait_for_instances: - wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'], 'viable_instances') - - props = get_properties(as_group) - instances = props['instances'] - if replace_all_instances: - # If replacing all instances, then set replace_instances to current set - # This allows replace_instances and replace_all_instances to behave same - replace_instances = instances - if replace_instances: - instances = replace_instances - - # check to see if instances are replaceable if checking launch configs - if launch_config_name: - new_instances, old_instances = get_instances_by_launch_config(props, lc_check, instances) - elif launch_template: - new_instances, old_instances = get_instances_by_launch_template(props, lt_check, instances) - - num_new_inst_needed = desired_capacity - len(new_instances) - - if lc_check or lt_check: - if num_new_inst_needed == 0 and old_instances: - module.debug("No new instances needed, but old instances are present. Removing old instances") - terminate_batch(connection, old_instances, instances, True) - as_group = describe_autoscaling_groups(connection, group_name)[0] - props = get_properties(as_group) - changed = True - return changed, props - - # we don't want to spin up extra instances if not necessary - if num_new_inst_needed < batch_size: - module.debug("Overriding batch size to %s" % num_new_inst_needed) - batch_size = num_new_inst_needed - - if not old_instances: - changed = False - return changed, props - - # check if min_size/max_size/desired capacity have been specified and if not use ASG values - if min_size is None: - min_size = as_group['MinSize'] - if max_size is None: - max_size = as_group['MaxSize'] - - # set temporary settings and wait for them to be reached - # This should get overwritten if the number of instances left is less than the batch size. - - as_group = describe_autoscaling_groups(connection, group_name)[0] - update_size(connection, as_group, max_size + batch_size, min_size + batch_size, desired_capacity + batch_size) - - if wait_for_instances: - wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'] + batch_size, 'viable_instances') - wait_for_elb(connection, group_name) - wait_for_target_group(connection, group_name) - - as_group = describe_autoscaling_groups(connection, group_name)[0] - props = get_properties(as_group) - instances = props['instances'] - if replace_instances: - instances = replace_instances - - module.debug("beginning main loop") - for i in get_chunks(instances, batch_size): - # break out of this loop if we have enough new instances - break_early, desired_size, term_instances = terminate_batch(connection, i, instances, False) - - if wait_for_instances: - wait_for_term_inst(connection, term_instances) - wait_for_new_inst(connection, group_name, wait_timeout, desired_size, 'viable_instances') - wait_for_elb(connection, group_name) - wait_for_target_group(connection, group_name) - - if break_early: - module.debug("breaking loop") - break - - update_size(connection, as_group, max_size, min_size, desired_capacity) - as_group = describe_autoscaling_groups(connection, group_name)[0] - asg_properties = get_properties(as_group) - module.debug("Rolling update complete.") - changed = True - return changed, asg_properties - - -def get_instances_by_launch_config(props, lc_check, initial_instances): - new_instances = [] - old_instances = [] - # old instances are those that have the old launch config - if lc_check: - for i in props['instances']: - # Check if migrating from launch_template to launch_config first - if 'launch_template' in props['instance_facts'][i]: - old_instances.append(i) - elif props['instance_facts'][i].get('launch_config_name') == props['launch_config_name']: - new_instances.append(i) - else: - old_instances.append(i) - - else: - module.debug("Comparing initial instances with current: %s" % initial_instances) - for i in props['instances']: - if i not in initial_instances: - new_instances.append(i) - else: - old_instances.append(i) - - module.debug("New instances: %s, %s" % (len(new_instances), new_instances)) - module.debug("Old instances: %s, %s" % (len(old_instances), old_instances)) - - return new_instances, old_instances - - -def get_instances_by_launch_template(props, lt_check, initial_instances): - new_instances = [] - old_instances = [] - # old instances are those that have the old launch template or version of the same launch template - if lt_check: - for i in props['instances']: - # Check if migrating from launch_config_name to launch_template_name first - if 'launch_config_name' in props['instance_facts'][i]: - old_instances.append(i) - elif props['instance_facts'][i].get('launch_template') == props['launch_template']: - new_instances.append(i) - else: - old_instances.append(i) - else: - module.debug("Comparing initial instances with current: %s" % initial_instances) - for i in props['instances']: - if i not in initial_instances: - new_instances.append(i) - else: - old_instances.append(i) - - module.debug("New instances: %s, %s" % (len(new_instances), new_instances)) - module.debug("Old instances: %s, %s" % (len(old_instances), old_instances)) - - return new_instances, old_instances - - -def list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances): - instances_to_terminate = [] - instances = (inst_id for inst_id in replace_instances if inst_id in props['instances']) - # check to make sure instances given are actually in the given ASG - # and they have a non-current launch config - if 'launch_config_name' in module.params: - if lc_check: - for i in instances: - if ( - 'launch_template' in props['instance_facts'][i] - or props['instance_facts'][i]['launch_config_name'] != props['launch_config_name'] - ): - instances_to_terminate.append(i) - else: - for i in instances: - if i in initial_instances: - instances_to_terminate.append(i) - elif 'launch_template' in module.params: - if lt_check: - for i in instances: - if ( - 'launch_config_name' in props['instance_facts'][i] - or props['instance_facts'][i]['launch_template'] != props['launch_template'] - ): - instances_to_terminate.append(i) - else: - for i in instances: - if i in initial_instances: - instances_to_terminate.append(i) - - return instances_to_terminate - - -def terminate_batch(connection, replace_instances, initial_instances, leftovers=False): - batch_size = module.params.get('replace_batch_size') - min_size = module.params.get('min_size') - desired_capacity = module.params.get('desired_capacity') - group_name = module.params.get('name') - lc_check = module.params.get('lc_check') - lt_check = module.params.get('lt_check') - decrement_capacity = False - break_loop = False - - as_group = describe_autoscaling_groups(connection, group_name)[0] - if desired_capacity is None: - desired_capacity = as_group['DesiredCapacity'] - - props = get_properties(as_group) - desired_size = as_group['MinSize'] - if module.params.get('launch_config_name'): - new_instances, old_instances = get_instances_by_launch_config(props, lc_check, initial_instances) - else: - new_instances, old_instances = get_instances_by_launch_template(props, lt_check, initial_instances) - num_new_inst_needed = desired_capacity - len(new_instances) - - # check to make sure instances given are actually in the given ASG - # and they have a non-current launch config - instances_to_terminate = list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances) - - module.debug("new instances needed: %s" % num_new_inst_needed) - module.debug("new instances: %s" % new_instances) - module.debug("old instances: %s" % old_instances) - module.debug("batch instances: %s" % ",".join(instances_to_terminate)) - - if num_new_inst_needed == 0: - decrement_capacity = True - if as_group['MinSize'] != min_size: - if min_size is None: - min_size = as_group['MinSize'] - updated_params = dict(AutoScalingGroupName=as_group['AutoScalingGroupName'], MinSize=min_size) - update_asg(connection, **updated_params) - module.debug("Updating minimum size back to original of %s" % min_size) - # if are some leftover old instances, but we are already at capacity with new ones - # we don't want to decrement capacity - if leftovers: - decrement_capacity = False - break_loop = True - instances_to_terminate = old_instances - desired_size = min_size - module.debug("No new instances needed") - - if num_new_inst_needed < batch_size and num_new_inst_needed != 0: - instances_to_terminate = instances_to_terminate[:num_new_inst_needed] - decrement_capacity = False - break_loop = False - module.debug("%s new instances needed" % num_new_inst_needed) - - module.debug("decrementing capacity: %s" % decrement_capacity) - - for instance_id in instances_to_terminate: - elb_dreg(connection, group_name, instance_id) - module.debug("terminating instance: %s" % instance_id) - terminate_asg_instance(connection, instance_id, decrement_capacity) - - # we wait to make sure the machines we marked as Unhealthy are - # no longer in the list - - return break_loop, desired_size, instances_to_terminate - - -def wait_for_term_inst(connection, term_instances): - wait_timeout = module.params.get('wait_timeout') - group_name = module.params.get('name') - as_group = describe_autoscaling_groups(connection, group_name)[0] - count = 1 - wait_timeout = time.time() + wait_timeout - while wait_timeout > time.time() and count > 0: - module.debug("waiting for instances to terminate") - count = 0 - as_group = describe_autoscaling_groups(connection, group_name)[0] - props = get_properties(as_group) - instance_facts = props['instance_facts'] - instances = (i for i in instance_facts if i in term_instances) - for i in instances: - lifecycle = instance_facts[i]['lifecycle_state'] - health = instance_facts[i]['health_status'] - module.debug("Instance %s has state of %s,%s" % (i, lifecycle, health)) - if lifecycle.startswith('Terminating') or health == 'Unhealthy': - count += 1 - time.sleep(10) - - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime()) - - -def wait_for_new_inst(connection, group_name, wait_timeout, desired_size, prop): - # make sure we have the latest stats after that last loop. - as_group = describe_autoscaling_groups(connection, group_name)[0] - props = get_properties(as_group) - module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop])) - # now we make sure that we have enough instances in a viable state - wait_timeout = time.time() + wait_timeout - while wait_timeout > time.time() and desired_size > props[prop]: - module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop])) - time.sleep(10) - as_group = describe_autoscaling_groups(connection, group_name)[0] - props = get_properties(as_group) - if wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="Waited too long for new instances to become viable. %s" % time.asctime()) - module.debug("Reached %s: %s" % (prop, desired_size)) - return props - - -def asg_exists(connection): - group_name = module.params.get('name') - as_group = describe_autoscaling_groups(connection, group_name) - return bool(len(as_group)) - - -def main(): - argument_spec = dict( - name=dict(required=True, type='str'), - load_balancers=dict(type='list'), - target_group_arns=dict(type='list'), - availability_zones=dict(type='list'), - launch_config_name=dict(type='str'), - launch_template=dict( - type='dict', - default=None, - options=dict( - version=dict(type='str'), - launch_template_name=dict(type='str'), - launch_template_id=dict(type='str'), - ) - ), - min_size=dict(type='int'), - max_size=dict(type='int'), - max_instance_lifetime=dict(type='int'), - mixed_instances_policy=dict( - type='dict', - default=None, - options=dict( - instance_types=dict( - type='list', - elements='str' - ), - ) - ), - placement_group=dict(type='str'), - desired_capacity=dict(type='int'), - vpc_zone_identifier=dict(type='list'), - replace_batch_size=dict(type='int', default=1), - replace_all_instances=dict(type='bool', default=False), - replace_instances=dict(type='list', default=[]), - lc_check=dict(type='bool', default=True), - lt_check=dict(type='bool', default=True), - wait_timeout=dict(type='int', default=300), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(type='list', default=[]), - health_check_period=dict(type='int', default=300), - health_check_type=dict(default='EC2', choices=['EC2', 'ELB']), - default_cooldown=dict(type='int', default=300), - wait_for_instances=dict(type='bool', default=True), - termination_policies=dict(type='list', default='Default'), - notification_topic=dict(type='str', default=None), - notification_types=dict( - type='list', - default=[ - 'autoscaling:EC2_INSTANCE_LAUNCH', - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', - 'autoscaling:EC2_INSTANCE_TERMINATE', - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' - ] - ), - suspend_processes=dict(type='list', default=[]), - metrics_collection=dict(type='bool', default=False), - metrics_granularity=dict(type='str', default='1Minute'), - metrics_list=dict( - type='list', - default=[ - 'GroupMinSize', - 'GroupMaxSize', - 'GroupDesiredCapacity', - 'GroupInServiceInstances', - 'GroupPendingInstances', - 'GroupStandbyInstances', - 'GroupTerminatingInstances', - 'GroupTotalInstances' - ] - ) - ) - - global module - module = AnsibleAWSModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['replace_all_instances', 'replace_instances'], - ['launch_config_name', 'launch_template'] - ] - ) - - if ( - module.params.get('max_instance_lifetime') is not None - and not module.botocore_at_least('1.13.21') - ): - module.fail_json( - msg='Botocore needs to be version 1.13.21 or higher to use max_instance_lifetime.' - ) - - if ( - module.params.get('mixed_instances_policy') is not None - and not module.botocore_at_least('1.12.45') - ): - module.fail_json( - msg='Botocore needs to be version 1.12.45 or higher to use mixed_instances_policy.' - ) - - state = module.params.get('state') - replace_instances = module.params.get('replace_instances') - replace_all_instances = module.params.get('replace_all_instances') - - connection = module.client('autoscaling') - changed = create_changed = replace_changed = False - exists = asg_exists(connection) - - if state == 'present': - create_changed, asg_properties = create_autoscaling_group(connection) - elif state == 'absent': - changed = delete_autoscaling_group(connection) - module.exit_json(changed=changed) - - # Only replace instances if asg existed at start of call - if ( - exists - and (replace_all_instances or replace_instances) - and (module.params.get('launch_config_name') or module.params.get('launch_template')) - ): - replace_changed, asg_properties = replace(connection) - - if create_changed or replace_changed: - changed = True - - module.exit_json(changed=changed, **asg_properties) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg_info.py b/lib/ansible/modules/cloud/amazon/ec2_asg_info.py deleted file mode 100644 index 283f68b0d3..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_asg_info.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_asg_info -short_description: Gather information about ec2 Auto Scaling Groups (ASGs) in AWS -description: - - Gather information about ec2 Auto Scaling Groups (ASGs) in AWS - - This module was called C(ec2_asg_facts) before Ansible 2.9. The usage did not change. -version_added: "2.2" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - name: - description: - - The prefix or name of the auto scaling group(s) you are searching for. - - "Note: This is a regular expression match with implicit '^' (beginning of string). Append '$' for a complete name match." - type: str - required: false - tags: - description: - - > - A dictionary/hash of tags in the format { tag1_name: 'tag1_value', tag2_name: 'tag2_value' } to match against the auto scaling - group(s) you are searching for. - required: false - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Find all groups -- ec2_asg_info: - register: asgs - -# Find a group with matching name/prefix -- ec2_asg_info: - name: public-webserver-asg - register: asgs - -# Find a group with matching tags -- ec2_asg_info: - tags: - project: webapp - env: production - register: asgs - -# Find a group with matching name/prefix and tags -- ec2_asg_info: - name: myproject - tags: - env: production - register: asgs - -# Fail if no groups are found -- ec2_asg_info: - name: public-webserver-asg - register: asgs - failed_when: "{{ asgs.results | length == 0 }}" - -# Fail if more than 1 group is found -- ec2_asg_info: - name: public-webserver-asg - register: asgs - failed_when: "{{ asgs.results | length > 1 }}" -''' - -RETURN = ''' ---- -auto_scaling_group_arn: - description: The Amazon Resource Name of the ASG - returned: success - type: str - sample: "arn:aws:autoscaling:us-west-2:1234567890:autoScalingGroup:10787c52-0bcb-427d-82ba-c8e4b008ed2e:autoScalingGroupName/public-webapp-production-1" -auto_scaling_group_name: - description: Name of autoscaling group - returned: success - type: str - sample: "public-webapp-production-1" -availability_zones: - description: List of Availability Zones that are enabled for this ASG. - returned: success - type: list - sample: ["us-west-2a", "us-west-2b", "us-west-2a"] -created_time: - description: The date and time this ASG was created, in ISO 8601 format. - returned: success - type: str - sample: "2015-11-25T00:05:36.309Z" -default_cooldown: - description: The default cooldown time in seconds. - returned: success - type: int - sample: 300 -desired_capacity: - description: The number of EC2 instances that should be running in this group. - returned: success - type: int - sample: 3 -health_check_period: - description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. - returned: success - type: int - sample: 30 -health_check_type: - description: The service you want the health status from, one of "EC2" or "ELB". - returned: success - type: str - sample: "ELB" -instances: - description: List of EC2 instances and their status as it relates to the ASG. - returned: success - type: list - sample: [ - { - "availability_zone": "us-west-2a", - "health_status": "Healthy", - "instance_id": "i-es22ad25", - "launch_configuration_name": "public-webapp-production-1", - "lifecycle_state": "InService", - "protected_from_scale_in": "false" - } - ] -launch_config_name: - description: > - Name of launch configuration associated with the ASG. Same as launch_configuration_name, - provided for compatibility with ec2_asg module. - returned: success - type: str - sample: "public-webapp-production-1" -launch_configuration_name: - description: Name of launch configuration associated with the ASG. - returned: success - type: str - sample: "public-webapp-production-1" -load_balancer_names: - description: List of load balancers names attached to the ASG. - returned: success - type: list - sample: ["elb-webapp-prod"] -max_size: - description: Maximum size of group - returned: success - type: int - sample: 3 -min_size: - description: Minimum size of group - returned: success - type: int - sample: 1 -new_instances_protected_from_scale_in: - description: Whether or not new instances a protected from automatic scaling in. - returned: success - type: bool - sample: "false" -placement_group: - description: Placement group into which instances are launched, if any. - returned: success - type: str - sample: None -status: - description: The current state of the group when DeleteAutoScalingGroup is in progress. - returned: success - type: str - sample: None -tags: - description: List of tags for the ASG, and whether or not each tag propagates to instances at launch. - returned: success - type: list - sample: [ - { - "key": "Name", - "value": "public-webapp-production-1", - "resource_id": "public-webapp-production-1", - "resource_type": "auto-scaling-group", - "propagate_at_launch": "true" - }, - { - "key": "env", - "value": "production", - "resource_id": "public-webapp-production-1", - "resource_type": "auto-scaling-group", - "propagate_at_launch": "true" - } - ] -target_group_arns: - description: List of ARNs of the target groups that the ASG populates - returned: success - type: list - sample: [ - "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b", - "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234" - ] -target_group_names: - description: List of names of the target groups that the ASG populates - returned: success - type: list - sample: [ - "target-group-host-hello", - "target-group-path-world" - ] -termination_policies: - description: A list of termination policies for the group. - returned: success - type: str - sample: ["Default"] -''' - -import re - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -def match_asg_tags(tags_to_match, asg): - for key, value in tags_to_match.items(): - for tag in asg['Tags']: - if key == tag['Key'] and value == tag['Value']: - break - else: - return False - return True - - -def find_asgs(conn, module, name=None, tags=None): - """ - Args: - conn (boto3.AutoScaling.Client): Valid Boto3 ASG client. - name (str): Optional name of the ASG you are looking for. - tags (dict): Optional dictionary of tags and values to search for. - - Basic Usage: - >>> name = 'public-webapp-production' - >>> tags = { 'env': 'production' } - >>> conn = boto3.client('autoscaling', region_name='us-west-2') - >>> results = find_asgs(name, conn) - - Returns: - List - [ - { - "auto_scaling_group_arn": ( - "arn:aws:autoscaling:us-west-2:275977225706:autoScalingGroup:58abc686-9783-4528-b338-3ad6f1cbbbaf:" - "autoScalingGroupName/public-webapp-production" - ), - "auto_scaling_group_name": "public-webapp-production", - "availability_zones": ["us-west-2c", "us-west-2b", "us-west-2a"], - "created_time": "2016-02-02T23:28:42.481000+00:00", - "default_cooldown": 300, - "desired_capacity": 2, - "enabled_metrics": [], - "health_check_grace_period": 300, - "health_check_type": "ELB", - "instances": - [ - { - "availability_zone": "us-west-2c", - "health_status": "Healthy", - "instance_id": "i-047a12cb", - "launch_configuration_name": "public-webapp-production-1", - "lifecycle_state": "InService", - "protected_from_scale_in": false - }, - { - "availability_zone": "us-west-2a", - "health_status": "Healthy", - "instance_id": "i-7a29df2c", - "launch_configuration_name": "public-webapp-production-1", - "lifecycle_state": "InService", - "protected_from_scale_in": false - } - ], - "launch_config_name": "public-webapp-production-1", - "launch_configuration_name": "public-webapp-production-1", - "load_balancer_names": ["public-webapp-production-lb"], - "max_size": 4, - "min_size": 2, - "new_instances_protected_from_scale_in": false, - "placement_group": None, - "status": None, - "suspended_processes": [], - "tags": - [ - { - "key": "Name", - "propagate_at_launch": true, - "resource_id": "public-webapp-production", - "resource_type": "auto-scaling-group", - "value": "public-webapp-production" - }, - { - "key": "env", - "propagate_at_launch": true, - "resource_id": "public-webapp-production", - "resource_type": "auto-scaling-group", - "value": "production" - } - ], - "target_group_names": [], - "target_group_arns": [], - "termination_policies": - [ - "Default" - ], - "vpc_zone_identifier": - [ - "subnet-a1b1c1d1", - "subnet-a2b2c2d2", - "subnet-a3b3c3d3" - ] - } - ] - """ - - try: - asgs_paginator = conn.get_paginator('describe_auto_scaling_groups') - asgs = asgs_paginator.paginate().build_full_result() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to describe AutoScalingGroups') - - if not asgs: - return asgs - - try: - elbv2 = module.client('elbv2') - except ClientError as e: - # This is nice to have, not essential - elbv2 = None - matched_asgs = [] - - if name is not None: - # if the user didn't specify a name - name_prog = re.compile(r'^' + name) - - for asg in asgs['AutoScalingGroups']: - if name: - matched_name = name_prog.search(asg['AutoScalingGroupName']) - else: - matched_name = True - - if tags: - matched_tags = match_asg_tags(tags, asg) - else: - matched_tags = True - - if matched_name and matched_tags: - asg = camel_dict_to_snake_dict(asg) - # compatibility with ec2_asg module - if 'launch_configuration_name' in asg: - asg['launch_config_name'] = asg['launch_configuration_name'] - # workaround for https://github.com/ansible/ansible/pull/25015 - if 'target_group_ar_ns' in asg: - asg['target_group_arns'] = asg['target_group_ar_ns'] - del(asg['target_group_ar_ns']) - if asg.get('target_group_arns'): - if elbv2: - try: - tg_paginator = elbv2.get_paginator('describe_target_groups') - tg_result = tg_paginator.paginate(TargetGroupArns=asg['target_group_arns']).build_full_result() - asg['target_group_names'] = [tg['TargetGroupName'] for tg in tg_result['TargetGroups']] - except ClientError as e: - if e.response['Error']['Code'] == 'TargetGroupNotFound': - asg['target_group_names'] = [] - else: - module.fail_json_aws(e, msg="Failed to describe Target Groups") - except BotoCoreError as e: - module.fail_json_aws(e, msg="Failed to describe Target Groups") - else: - asg['target_group_names'] = [] - matched_asgs.append(asg) - - return matched_asgs - - -def main(): - - argument_spec = dict( - name=dict(type='str'), - tags=dict(type='dict'), - ) - module = AnsibleAWSModule(argument_spec=argument_spec) - if module._name == 'ec2_asg_facts': - module.deprecate("The 'ec2_asg_facts' module has been renamed to 'ec2_asg_info'", version='2.13') - - asg_name = module.params.get('name') - asg_tags = module.params.get('tags') - - autoscaling = module.client('autoscaling') - - results = find_asgs(autoscaling, module, name=asg_name, tags=asg_tags) - module.exit_json(results=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg_lifecycle_hook.py b/lib/ansible/modules/cloud/amazon/ec2_asg_lifecycle_hook.py deleted file mode 100644 index 2d99a56392..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_asg_lifecycle_hook.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = """ ---- -module: ec2_asg_lifecycle_hook -short_description: Create, delete or update AWS ASG Lifecycle Hooks. -description: - - Will create a new hook when I(state=present) and no given Hook is found. - - Will update an existing hook when I(state=present) and a Hook is found, but current and provided parameters differ. - - Will delete the hook when I(state=absent) and a Hook is found. -version_added: "2.5" -author: Igor 'Tsigankov' Eyrich (@tsiganenok) <tsiganenok@gmail.com> -options: - state: - description: - - Create or delete Lifecycle Hook. - - When I(state=present) updates existing hook or creates a new hook if not found. - choices: ['present', 'absent'] - default: present - type: str - lifecycle_hook_name: - description: - - The name of the lifecycle hook. - required: true - type: str - autoscaling_group_name: - description: - - The name of the Auto Scaling group to which you want to assign the lifecycle hook. - required: true - type: str - transition: - description: - - The instance state to which you want to attach the lifecycle hook. - - Required when I(state=present). - choices: ['autoscaling:EC2_INSTANCE_TERMINATING', 'autoscaling:EC2_INSTANCE_LAUNCHING'] - type: str - role_arn: - description: - - The ARN of the IAM role that allows the Auto Scaling group to publish to the specified notification target. - type: str - notification_target_arn: - description: - - The ARN of the notification target that Auto Scaling will use to notify you when an - instance is in the transition state for the lifecycle hook. - - This target can be either an SQS queue or an SNS topic. - - If you specify an empty string, this overrides the current ARN. - type: str - notification_meta_data: - description: - - Contains additional information that you want to include any time Auto Scaling sends a message to the notification target. - type: str - heartbeat_timeout: - description: - - The amount of time, in seconds, that can elapse before the lifecycle hook times out. - When the lifecycle hook times out, Auto Scaling performs the default action. - You can prevent the lifecycle hook from timing out by calling RecordLifecycleActionHeartbeat. - - By default Amazon AWS will use 3600 (1 hour) - type: int - default_result: - description: - - Defines the action the Auto Scaling group should take when the lifecycle hook timeout - elapses or if an unexpected failure occurs. - choices: ['ABANDON', 'CONTINUE'] - default: ABANDON - type: str -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3>=1.4.4 ] - -""" - -EXAMPLES = ''' -# Create / Update lifecycle hook -- ec2_asg_lifecycle_hook: - region: eu-central-1 - state: present - autoscaling_group_name: example - lifecycle_hook_name: example - transition: autoscaling:EC2_INSTANCE_LAUNCHING - heartbeat_timeout: 7000 - default_result: ABANDON - -# Delete lifecycle hook -- ec2_asg_lifecycle_hook: - region: eu-central-1 - state: absent - autoscaling_group_name: example - lifecycle_hook_name: example - -''' - -RETURN = ''' - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - - -def create_lifecycle_hook(connection, module): - changed = False - - lch_name = module.params.get('lifecycle_hook_name') - asg_name = module.params.get('autoscaling_group_name') - transition = module.params.get('transition') - role_arn = module.params.get('role_arn') - notification_target_arn = module.params.get('notification_target_arn') - notification_meta_data = module.params.get('notification_meta_data') - heartbeat_timeout = module.params.get('heartbeat_timeout') - default_result = module.params.get('default_result') - - lch_params = { - 'LifecycleHookName': lch_name, - 'AutoScalingGroupName': asg_name, - 'LifecycleTransition': transition - } - - if role_arn: - lch_params['RoleARN'] = role_arn - - if notification_target_arn: - lch_params['NotificationTargetARN'] = notification_target_arn - - if notification_meta_data: - lch_params['NotificationMetadata'] = notification_meta_data - - if heartbeat_timeout: - lch_params['HeartbeatTimeout'] = heartbeat_timeout - - if default_result: - lch_params['DefaultResult'] = default_result - - try: - existing_hook = connection.describe_lifecycle_hooks( - AutoScalingGroupName=asg_name, - LifecycleHookNames=[lch_name] - )['LifecycleHooks'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get Lifecycle Hook") - - if not existing_hook: - changed = True - else: - # GlobalTimeout is not configurable, but exists in response. - # Removing it helps to compare both dicts in order to understand - # what changes were done. - del(existing_hook[0]['GlobalTimeout']) - added, removed, modified, same = dict_compare(lch_params, existing_hook[0]) - if added or removed or modified: - changed = True - - if changed: - try: - connection.put_lifecycle_hook(**lch_params) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to create LifecycleHook") - - return(changed) - - -def dict_compare(d1, d2): - d1_keys = set(d1.keys()) - d2_keys = set(d2.keys()) - intersect_keys = d1_keys.intersection(d2_keys) - added = d1_keys - d2_keys - removed = d2_keys - d1_keys - modified = False - for key in d1: - if d1[key] != d2[key]: - modified = True - break - - same = set(o for o in intersect_keys if d1[o] == d2[o]) - return added, removed, modified, same - - -def delete_lifecycle_hook(connection, module): - changed = False - - lch_name = module.params.get('lifecycle_hook_name') - asg_name = module.params.get('autoscaling_group_name') - - try: - all_hooks = connection.describe_lifecycle_hooks( - AutoScalingGroupName=asg_name - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get Lifecycle Hooks") - - for hook in all_hooks['LifecycleHooks']: - if hook['LifecycleHookName'] == lch_name: - lch_params = { - 'LifecycleHookName': lch_name, - 'AutoScalingGroupName': asg_name - } - - try: - connection.delete_lifecycle_hook(**lch_params) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to delete LifecycleHook") - else: - pass - - return(changed) - - -def main(): - argument_spec = dict( - autoscaling_group_name=dict(required=True, type='str'), - lifecycle_hook_name=dict(required=True, type='str'), - transition=dict(type='str', choices=['autoscaling:EC2_INSTANCE_TERMINATING', 'autoscaling:EC2_INSTANCE_LAUNCHING']), - role_arn=dict(type='str'), - notification_target_arn=dict(type='str'), - notification_meta_data=dict(type='str'), - heartbeat_timeout=dict(type='int'), - default_result=dict(default='ABANDON', choices=['ABANDON', 'CONTINUE']), - state=dict(default='present', choices=['present', 'absent']) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['transition']]]) - state = module.params.get('state') - - connection = module.client('autoscaling') - - changed = False - - if state == 'present': - changed = create_lifecycle_hook(connection, module) - elif state == 'absent': - changed = delete_lifecycle_hook(connection, module) - - module.exit_json(changed=changed) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_customer_gateway.py b/lib/ansible/modules/cloud/amazon/ec2_customer_gateway.py deleted file mode 100644 index db62b76c2a..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_customer_gateway.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_customer_gateway -short_description: Manage an AWS customer gateway -description: - - Manage an AWS customer gateway. -version_added: "2.2" -author: Michael Baydoun (@MichaelBaydoun) -requirements: [ botocore, boto3 ] -notes: - - You cannot create more than one customer gateway with the same IP address. If you run an identical request more than one time, the - first request creates the customer gateway, and subsequent requests return information about the existing customer gateway. The subsequent - requests do not create new customer gateway resources. - - Return values contain customer_gateway and customer_gateways keys which are identical dicts. You should use - customer_gateway. See U(https://github.com/ansible/ansible-modules-extras/issues/2773) for details. -options: - bgp_asn: - description: - - Border Gateway Protocol (BGP) Autonomous System Number (ASN), required when I(state=present). - type: int - ip_address: - description: - - Internet-routable IP address for customers gateway, must be a static address. - required: true - type: str - name: - description: - - Name of the customer gateway. - required: true - type: str - routing: - description: - - The type of routing. - choices: ['static', 'dynamic'] - default: dynamic - version_added: '2.4' - type: str - state: - description: - - Create or terminate the Customer Gateway. - default: present - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' - -# Create Customer Gateway -- ec2_customer_gateway: - bgp_asn: 12345 - ip_address: 1.2.3.4 - name: IndianapolisOffice - region: us-east-1 - register: cgw - -# Delete Customer Gateway -- ec2_customer_gateway: - ip_address: 1.2.3.4 - name: IndianapolisOffice - state: absent - region: us-east-1 - register: cgw -''' - -RETURN = ''' -gateway.customer_gateways: - description: details about the gateway that was created. - returned: success - type: complex - contains: - bgp_asn: - description: The Border Gateway Autonomous System Number. - returned: when exists and gateway is available. - sample: 65123 - type: str - customer_gateway_id: - description: gateway id assigned by amazon. - returned: when exists and gateway is available. - sample: cgw-cb6386a2 - type: str - ip_address: - description: ip address of your gateway device. - returned: when exists and gateway is available. - sample: 1.2.3.4 - type: str - state: - description: state of gateway. - returned: when gateway exists and is available. - sample: available - type: str - tags: - description: Any tags on the gateway. - returned: when gateway exists and is available, and when tags exist. - type: list - type: - description: encryption type. - returned: when gateway exists and is available. - sample: ipsec.1 - type: str -''' - -try: - from botocore.exceptions import ClientError - HAS_BOTOCORE = True -except ImportError: - HAS_BOTOCORE = False - -try: - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, AWSRetry, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -class Ec2CustomerGatewayManager: - - def __init__(self, module): - self.module = module - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - self.ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except ClientError as e: - module.fail_json(msg=e.message) - - @AWSRetry.jittered_backoff(delay=2, max_delay=30, retries=6, catch_extra_error_codes=['IncorrectState']) - def ensure_cgw_absent(self, gw_id): - response = self.ec2.delete_customer_gateway( - DryRun=False, - CustomerGatewayId=gw_id - ) - return response - - def ensure_cgw_present(self, bgp_asn, ip_address): - if not bgp_asn: - bgp_asn = 65000 - response = self.ec2.create_customer_gateway( - DryRun=False, - Type='ipsec.1', - PublicIp=ip_address, - BgpAsn=bgp_asn, - ) - return response - - def tag_cgw_name(self, gw_id, name): - response = self.ec2.create_tags( - DryRun=False, - Resources=[ - gw_id, - ], - Tags=[ - { - 'Key': 'Name', - 'Value': name - }, - ] - ) - return response - - def describe_gateways(self, ip_address): - response = self.ec2.describe_customer_gateways( - DryRun=False, - Filters=[ - { - 'Name': 'state', - 'Values': [ - 'available', - ] - }, - { - 'Name': 'ip-address', - 'Values': [ - ip_address, - ] - } - ] - ) - return response - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - bgp_asn=dict(required=False, type='int'), - ip_address=dict(required=True), - name=dict(required=True), - routing=dict(default='dynamic', choices=['dynamic', 'static']), - state=dict(default='present', choices=['present', 'absent']), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True, - required_if=[ - ('routing', 'dynamic', ['bgp_asn']) - ] - ) - - if not HAS_BOTOCORE: - module.fail_json(msg='botocore is required.') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - gw_mgr = Ec2CustomerGatewayManager(module) - - name = module.params.get('name') - - existing = gw_mgr.describe_gateways(module.params['ip_address']) - - results = dict(changed=False) - if module.params['state'] == 'present': - if existing['CustomerGateways']: - existing['CustomerGateway'] = existing['CustomerGateways'][0] - results['gateway'] = existing - if existing['CustomerGateway']['Tags']: - tag_array = existing['CustomerGateway']['Tags'] - for key, value in enumerate(tag_array): - if value['Key'] == 'Name': - current_name = value['Value'] - if current_name != name: - results['name'] = gw_mgr.tag_cgw_name( - results['gateway']['CustomerGateway']['CustomerGatewayId'], - module.params['name'], - ) - results['changed'] = True - else: - if not module.check_mode: - results['gateway'] = gw_mgr.ensure_cgw_present( - module.params['bgp_asn'], - module.params['ip_address'], - ) - results['name'] = gw_mgr.tag_cgw_name( - results['gateway']['CustomerGateway']['CustomerGatewayId'], - module.params['name'], - ) - results['changed'] = True - - elif module.params['state'] == 'absent': - if existing['CustomerGateways']: - existing['CustomerGateway'] = existing['CustomerGateways'][0] - results['gateway'] = existing - if not module.check_mode: - results['gateway'] = gw_mgr.ensure_cgw_absent( - existing['CustomerGateway']['CustomerGatewayId'] - ) - results['changed'] = True - - pretty_results = camel_dict_to_snake_dict(results) - module.exit_json(**pretty_results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_customer_gateway_info.py b/lib/ansible/modules/cloud/amazon/ec2_customer_gateway_info.py deleted file mode 100644 index 90d5130059..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_customer_gateway_info.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: ec2_customer_gateway_info -short_description: Gather information about customer gateways in AWS -description: - - Gather information about customer gateways in AWS. - - This module was called C(ec2_customer_gateway_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -requirements: [ boto3 ] -author: Madhura Naniwadekar (@Madhura-CSI) -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeCustomerGateways.html) for possible filters. - type: dict - customer_gateway_ids: - description: - - Get details of a specific customer gateways using customer gateway ID/IDs. This value should be provided as a list. - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# # Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Gather information about all customer gateways - ec2_customer_gateway_info: - -- name: Gather information about a filtered list of customer gateways, based on tags - ec2_customer_gateway_info: - region: ap-southeast-2 - filters: - "tag:Name": test-customer-gateway - "tag:AltName": test-customer-gateway-alt - register: cust_gw_info - -- name: Gather information about a specific customer gateway by specifying customer gateway ID - ec2_customer_gateway_info: - region: ap-southeast-2 - customer_gateway_ids: - - 'cgw-48841a09' - - 'cgw-fec021ce' - register: cust_gw_info -''' - -RETURN = ''' -customer_gateways: - description: List of one or more customer gateways. - returned: always - type: list - sample: [ - { - "bgp_asn": "65000", - "customer_gateway_id": "cgw-fec844ce", - "customer_gateway_name": "test-customer-gw", - "ip_address": "110.112.113.120", - "state": "available", - "tags": [ - { - "key": "Name", - "value": "test-customer-gw" - } - ], - "type": "ipsec.1" - } - ] -''' - -import json -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def list_customer_gateways(connection, module): - params = dict() - - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - params['CustomerGatewayIds'] = module.params.get('customer_gateway_ids') - - try: - result = json.loads(json.dumps(connection.describe_customer_gateways(**params), default=date_handler)) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not describe customer gateways") - snaked_customer_gateways = [camel_dict_to_snake_dict(gateway) for gateway in result['CustomerGateways']] - if snaked_customer_gateways: - for customer_gateway in snaked_customer_gateways: - customer_gateway['tags'] = boto3_tag_list_to_ansible_dict(customer_gateway.get('tags', [])) - customer_gateway_name = customer_gateway['tags'].get('Name') - if customer_gateway_name: - customer_gateway['customer_gateway_name'] = customer_gateway_name - module.exit_json(changed=False, customer_gateways=snaked_customer_gateways) - - -def main(): - - argument_spec = dict( - customer_gateway_ids=dict(default=[], type='list'), - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - mutually_exclusive=[['customer_gateway_ids', 'filters']], - supports_check_mode=True) - if module._module._name == 'ec2_customer_gateway_facts': - module._module.deprecate("The 'ec2_customer_gateway_facts' module has been renamed to 'ec2_customer_gateway_info'", version='2.13') - - connection = module.client('ec2') - - list_customer_gateways(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_eip.py b/lib/ansible/modules/cloud/amazon/ec2_eip.py deleted file mode 100644 index 7f7cd18c5b..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_eip.py +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eip -short_description: manages EC2 elastic IP (EIP) addresses. -description: - - This module can allocate or release an EIP. - - This module can associate/disassociate an EIP with instances or network interfaces. -version_added: "1.4" -options: - device_id: - description: - - The id of the device for the EIP. Can be an EC2 Instance id or Elastic Network Interface (ENI) id. - required: false - aliases: [ instance_id ] - version_added: "2.0" - type: str - public_ip: - description: - - The IP address of a previously allocated EIP. - - When I(public_ip=present) and device is specified, the EIP is associated with the device. - - When I(public_ip=absent) and device is specified, the EIP is disassociated from the device. - aliases: [ ip ] - type: str - state: - description: - - When C(state=present), allocate an EIP or associate an existing EIP with a device. - - When C(state=absent), disassociate the EIP from the device and optionally release it. - choices: ['present', 'absent'] - default: present - type: str - in_vpc: - description: - - Allocate an EIP inside a VPC or not. - - Required if specifying an ENI with I(device_id). - default: false - type: bool - version_added: "1.4" - reuse_existing_ip_allowed: - description: - - Reuse an EIP that is not associated to a device (when available), instead of allocating a new one. - default: false - type: bool - version_added: "1.6" - release_on_disassociation: - description: - - Whether or not to automatically release the EIP when it is disassociated. - default: false - type: bool - version_added: "2.0" - private_ip_address: - description: - - The primary or secondary private IP address to associate with the Elastic IP address. - version_added: "2.3" - type: str - allow_reassociation: - description: - - Specify this option to allow an Elastic IP address that is already associated with another - network interface or instance to be re-associated with the specified instance or interface. - default: false - type: bool - version_added: "2.5" - tag_name: - description: - - When I(reuse_existing_ip_allowed=true), supplement with this option to only reuse - an Elastic IP if it is tagged with I(tag_name). - version_added: "2.9" - type: str - tag_value: - description: - - Supplements I(tag_name) but also checks that the value of the tag provided in I(tag_name) matches I(tag_value). - version_added: "2.9" - type: str - public_ipv4_pool: - description: - - Allocates the new Elastic IP from the provided public IPv4 pool (BYOIP) - only applies to newly allocated Elastic IPs, isn't validated when I(reuse_existing_ip_allowed=true). - version_added: "2.9" - type: str - wait_timeout: - description: - - The I(wait_timeout) option does nothing and will be removed in Ansible 2.14. - type: int -extends_documentation_fragment: - - aws - - ec2 -author: "Rick Mendes (@rickmendes) <rmendes@illumina.com>" -notes: - - There may be a delay between the time the EIP is assigned and when - the cloud instance is reachable via the new address. Use wait_for and - pause to delay further playbook execution until the instance is reachable, - if necessary. - - This module returns multiple changed statuses on disassociation or release. - It returns an overall status based on any changes occurring. It also returns - individual changed statuses for disassociation and release. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: associate an elastic IP with an instance - ec2_eip: - device_id: i-1212f003 - ip: 93.184.216.119 - -- name: associate an elastic IP with a device - ec2_eip: - device_id: eni-c8ad70f3 - ip: 93.184.216.119 - -- name: associate an elastic IP with a device and allow reassociation - ec2_eip: - device_id: eni-c8ad70f3 - public_ip: 93.184.216.119 - allow_reassociation: true - -- name: disassociate an elastic IP from an instance - ec2_eip: - device_id: i-1212f003 - ip: 93.184.216.119 - state: absent - -- name: disassociate an elastic IP with a device - ec2_eip: - device_id: eni-c8ad70f3 - ip: 93.184.216.119 - state: absent - -- name: allocate a new elastic IP and associate it with an instance - ec2_eip: - device_id: i-1212f003 - -- name: allocate a new elastic IP without associating it to anything - ec2_eip: - state: present - register: eip - -- name: output the IP - debug: - msg: "Allocated IP is {{ eip.public_ip }}" - -- name: provision new instances with ec2 - ec2: - keypair: mykey - instance_type: c1.medium - image: ami-40603AD1 - wait: true - group: webserver - count: 3 - register: ec2 - -- name: associate new elastic IPs with each of the instances - ec2_eip: - device_id: "{{ item }}" - loop: "{{ ec2.instance_ids }}" - -- name: allocate a new elastic IP inside a VPC in us-west-2 - ec2_eip: - region: us-west-2 - in_vpc: true - register: eip - -- name: output the IP - debug: - msg: "Allocated IP inside a VPC is {{ eip.public_ip }}" - -- name: allocate eip - reuse unallocated ips (if found) with FREE tag - ec2_eip: - region: us-east-1 - in_vpc: true - reuse_existing_ip_allowed: true - tag_name: FREE - -- name: allocate eip - reuse unallocted ips if tag reserved is nope - ec2_eip: - region: us-east-1 - in_vpc: true - reuse_existing_ip_allowed: true - tag_name: reserved - tag_value: nope - -- name: allocate new eip - from servers given ipv4 pool - ec2_eip: - region: us-east-1 - in_vpc: true - public_ipv4_pool: ipv4pool-ec2-0588c9b75a25d1a02 - -- name: allocate eip - from a given pool (if no free addresses where dev-servers tag is dynamic) - ec2_eip: - region: us-east-1 - in_vpc: true - reuse_existing_ip_allowed: true - tag_name: dev-servers - public_ipv4_pool: ipv4pool-ec2-0588c9b75a25d1a02 - -- name: allocate eip from pool - check if tag reserved_for exists and value is our hostname - ec2_eip: - region: us-east-1 - in_vpc: true - reuse_existing_ip_allowed: true - tag_name: reserved_for - tag_value: "{{ inventory_hostname }}" - public_ipv4_pool: ipv4pool-ec2-0588c9b75a25d1a02 -''' - -RETURN = ''' -allocation_id: - description: allocation_id of the elastic ip - returned: on success - type: str - sample: eipalloc-51aa3a6c -public_ip: - description: an elastic ip address - returned: on success - type: str - sample: 52.88.159.209 -''' - -try: - import botocore.exceptions -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import AWSRetry, ansible_dict_to_boto3_filter_list - - -def associate_ip_and_device(ec2, module, address, private_ip_address, device_id, allow_reassociation, check_mode, is_instance=True): - if address_is_associated_with_device(ec2, module, address, device_id, is_instance): - return {'changed': False} - - # If we're in check mode, nothing else to do - if not check_mode: - if is_instance: - try: - params = dict( - InstanceId=device_id, - AllowReassociation=allow_reassociation, - ) - if private_ip_address: - params['PrivateIPAddress'] = private_ip_address - if address['Domain'] == 'vpc': - params['AllocationId'] = address['AllocationId'] - else: - params['PublicIp'] = address['PublicIp'] - res = ec2.associate_address(**params) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - msg = "Couldn't associate Elastic IP address with instance '{0}'".format(device_id) - module.fail_json_aws(e, msg=msg) - else: - params = dict( - NetworkInterfaceId=device_id, - AllocationId=address['AllocationId'], - AllowReassociation=allow_reassociation, - ) - - if private_ip_address: - params['PrivateIpAddress'] = private_ip_address - - try: - res = ec2.associate_address(aws_retry=True, **params) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - msg = "Couldn't associate Elastic IP address with network interface '{0}'".format(device_id) - module.fail_json_aws(e, msg=msg) - if not res: - module.fail_json_aws(e, msg='Association failed.') - - return {'changed': True} - - -def disassociate_ip_and_device(ec2, module, address, device_id, check_mode, is_instance=True): - if not address_is_associated_with_device(ec2, module, address, device_id, is_instance): - return {'changed': False} - - # If we're in check mode, nothing else to do - if not check_mode: - try: - if address['Domain'] == 'vpc': - res = ec2.disassociate_address( - AssociationId=address['AssociationId'], aws_retry=True - ) - else: - res = ec2.disassociate_address( - PublicIp=address['PublicIp'], aws_retry=True - ) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Dissassociation of Elastic IP failed") - - return {'changed': True} - - -@AWSRetry.jittered_backoff() -def find_address(ec2, module, public_ip, device_id, is_instance=True): - """ Find an existing Elastic IP address """ - filters = [] - kwargs = {} - - if public_ip: - kwargs["PublicIps"] = [public_ip] - elif device_id: - if is_instance: - filters.append({"Name": 'instance-id', "Values": [device_id]}) - else: - filters.append({'Name': 'network-interface-id', "Values": [device_id]}) - - if len(filters) > 0: - kwargs["Filters"] = filters - elif len(filters) == 0 and public_ip is None: - return None - - try: - addresses = ec2.describe_addresses(**kwargs) - except is_boto3_error_code('InvalidAddress.NotFound') as e: - # If we're releasing and we can't find it, it's already gone... - if module.params.get('state') == 'absent': - module.exit_json(changed=False) - module.fail_json_aws(e, msg="Couldn't obtain list of existing Elastic IP addresses") - - addresses = addresses["Addresses"] - if len(addresses) == 1: - return addresses[0] - elif len(addresses) > 1: - msg = "Found more than one address using args {0}".format(kwargs) - msg += "Addresses found: {0}".format(addresses) - module.fail_json_aws(botocore.exceptions.ClientError, msg=msg) - - -def address_is_associated_with_device(ec2, module, address, device_id, is_instance=True): - """ Check if the elastic IP is currently associated with the device """ - address = find_address(ec2, module, address["PublicIp"], device_id, is_instance) - if address: - if is_instance: - if "InstanceId" in address and address["InstanceId"] == device_id: - return address - else: - if "NetworkInterfaceId" in address and address["NetworkInterfaceId"] == device_id: - return address - return False - - -def allocate_address(ec2, module, domain, reuse_existing_ip_allowed, check_mode, tag_dict=None, public_ipv4_pool=None): - """ Allocate a new elastic IP address (when needed) and return it """ - if reuse_existing_ip_allowed: - filters = [] - if not domain: - domain = 'standard' - filters.append({'Name': 'domain', "Values": [domain]}) - - if tag_dict is not None: - filters += ansible_dict_to_boto3_filter_list(tag_dict) - - try: - all_addresses = ec2.describe_addresses(Filters=filters, aws_retry=True) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't obtain list of existing Elastic IP addresses") - - all_addresses = all_addresses["Addresses"] - - if domain == 'vpc': - unassociated_addresses = [a for a in all_addresses - if not a.get('AssociationId', None)] - else: - unassociated_addresses = [a for a in all_addresses - if not a['InstanceId']] - if unassociated_addresses: - return unassociated_addresses[0], False - - if public_ipv4_pool: - return allocate_address_from_pool(ec2, module, domain, check_mode, public_ipv4_pool), True - - try: - result = ec2.allocate_address(Domain=domain, aws_retry=True), True - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't allocate Elastic IP address") - return result - - -def release_address(ec2, module, address, check_mode): - """ Release a previously allocated elastic IP address """ - - # If we're in check mode, nothing else to do - if not check_mode: - try: - result = ec2.release_address(AllocationId=address['AllocationId'], aws_retry=True) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't release Elastic IP address") - - return {'changed': True} - - -@AWSRetry.jittered_backoff() -def describe_eni_with_backoff(ec2, module, device_id): - try: - return ec2.describe_network_interfaces(NetworkInterfaceIds=[device_id]) - except is_boto3_error_code('InvalidNetworkInterfaceID.NotFound') as e: - module.fail_json_aws(e, msg="Couldn't get list of network interfaces.") - - -def find_device(ec2, module, device_id, is_instance=True): - """ Attempt to find the EC2 instance and return it """ - - if is_instance: - try: - paginator = ec2.get_paginator('describe_instances') - reservations = list(paginator.paginate(InstanceIds=[device_id]).search('Reservations[]')) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't get list of instances") - - if len(reservations) == 1: - instances = reservations[0]['Instances'] - if len(instances) == 1: - return instances[0] - else: - try: - interfaces = describe_eni_with_backoff(ec2, module, device_id) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't get list of network interfaces.") - if len(interfaces) == 1: - return interfaces[0] - - -def ensure_present(ec2, module, domain, address, private_ip_address, device_id, - reuse_existing_ip_allowed, allow_reassociation, check_mode, is_instance=True): - changed = False - - # Return the EIP object since we've been given a public IP - if not address: - if check_mode: - return {'changed': True} - - address, changed = allocate_address(ec2, module, domain, reuse_existing_ip_allowed, check_mode) - - if device_id: - # Allocate an IP for instance since no public_ip was provided - if is_instance: - instance = find_device(ec2, module, device_id) - if reuse_existing_ip_allowed: - if instance.vpc_id and len(instance.vpc_id) > 0 and domain is None: - msg = "You must set 'in_vpc' to true to associate an instance with an existing ip in a vpc" - module.fail_json_aws(botocore.exceptions.ClientError, msg=msg) - - # Associate address object (provided or allocated) with instance - assoc_result = associate_ip_and_device( - ec2, module, address, private_ip_address, device_id, allow_reassociation, - check_mode - ) - else: - instance = find_device(ec2, module, device_id, is_instance=False) - # Associate address object (provided or allocated) with instance - assoc_result = associate_ip_and_device( - ec2, module, address, private_ip_address, device_id, allow_reassociation, - check_mode, is_instance=False - ) - - changed = changed or assoc_result['changed'] - - return {'changed': changed, 'public_ip': address['PublicIp'], 'allocation_id': address['AllocationId']} - - -def ensure_absent(ec2, module, address, device_id, check_mode, is_instance=True): - if not address: - return {'changed': False} - - # disassociating address from instance - if device_id: - if is_instance: - return disassociate_ip_and_device( - ec2, module, address, device_id, check_mode - ) - else: - return disassociate_ip_and_device( - ec2, module, address, device_id, check_mode, is_instance=False - ) - # releasing address - else: - return release_address(ec2, module, address, check_mode) - - -def allocate_address_from_pool(ec2, module, domain, check_mode, public_ipv4_pool): - # type: (EC2Connection, str, bool, str) -> Address - """ Overrides boto's allocate_address function to support BYOIP """ - params = {} - - if domain is not None: - params['Domain'] = domain - - if public_ipv4_pool is not None: - params['PublicIpv4Pool'] = public_ipv4_pool - - if check_mode: - params['DryRun'] = 'true' - - try: - result = ec2.allocate_address(aws_retry=True, **params) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't allocate Elastic IP address") - return result - - -def generate_tag_dict(module, tag_name, tag_value): - # type: (AnsibleModule, str, str) -> Optional[Dict] - """ Generates a dictionary to be passed as a filter to Amazon """ - if tag_name and not tag_value: - if tag_name.startswith('tag:'): - tag_name = tag_name.strip('tag:') - return {'tag-key': tag_name} - - elif tag_name and tag_value: - if not tag_name.startswith('tag:'): - tag_name = 'tag:' + tag_name - return {tag_name: tag_value} - - elif tag_value and not tag_name: - module.fail_json(msg="parameters are required together: ('tag_name', 'tag_value')") - - -def main(): - argument_spec = dict( - device_id=dict(required=False, aliases=['instance_id']), - public_ip=dict(required=False, aliases=['ip']), - state=dict(required=False, default='present', - choices=['present', 'absent']), - in_vpc=dict(required=False, type='bool', default=False), - reuse_existing_ip_allowed=dict(required=False, type='bool', - default=False), - release_on_disassociation=dict(required=False, type='bool', default=False), - allow_reassociation=dict(type='bool', default=False), - wait_timeout=dict(type='int', removed_in_version='2.14'), - private_ip_address=dict(), - tag_name=dict(), - tag_value=dict(), - public_ipv4_pool=dict() - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_by={ - 'private_ip_address': ['device_id'], - }, - ) - - ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - - device_id = module.params.get('device_id') - instance_id = module.params.get('instance_id') - public_ip = module.params.get('public_ip') - private_ip_address = module.params.get('private_ip_address') - state = module.params.get('state') - in_vpc = module.params.get('in_vpc') - domain = 'vpc' if in_vpc else None - reuse_existing_ip_allowed = module.params.get('reuse_existing_ip_allowed') - release_on_disassociation = module.params.get('release_on_disassociation') - allow_reassociation = module.params.get('allow_reassociation') - tag_name = module.params.get('tag_name') - tag_value = module.params.get('tag_value') - public_ipv4_pool = module.params.get('public_ipv4_pool') - - if instance_id: - warnings = ["instance_id is no longer used, please use device_id going forward"] - is_instance = True - device_id = instance_id - else: - if device_id and device_id.startswith('i-'): - is_instance = True - elif device_id: - if device_id.startswith('eni-') and not in_vpc: - module.fail_json(msg="If you are specifying an ENI, in_vpc must be true") - is_instance = False - - tag_dict = generate_tag_dict(module, tag_name, tag_value) - - try: - if device_id: - address = find_address(ec2, module, public_ip, device_id, is_instance=is_instance) - else: - address = find_address(ec2, module, public_ip, None) - - if state == 'present': - if device_id: - result = ensure_present( - ec2, module, domain, address, private_ip_address, device_id, - reuse_existing_ip_allowed, allow_reassociation, - module.check_mode, is_instance=is_instance - ) - else: - if address: - changed = False - else: - address, changed = allocate_address( - ec2, module, domain, reuse_existing_ip_allowed, - module.check_mode, tag_dict, public_ipv4_pool - ) - result = { - 'changed': changed, - 'public_ip': address['PublicIp'], - 'allocation_id': address['AllocationId'] - } - else: - if device_id: - disassociated = ensure_absent( - ec2, module, address, device_id, module.check_mode, is_instance=is_instance - ) - - if release_on_disassociation and disassociated['changed']: - released = release_address(ec2, module, address, module.check_mode) - result = { - 'changed': True, - 'disassociated': disassociated, - 'released': released - } - else: - result = { - 'changed': disassociated['changed'], - 'disassociated': disassociated, - 'released': {'changed': False} - } - else: - released = release_address(ec2, module, address, module.check_mode) - result = { - 'changed': released['changed'], - 'disassociated': {'changed': False}, - 'released': released - } - - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(str(e)) - - if instance_id: - result['warnings'] = warnings - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_eip_info.py b/lib/ansible/modules/cloud/amazon/ec2_eip_info.py deleted file mode 100644 index 27fda74903..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_eip_info.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eip_info -short_description: List EC2 EIP details -description: - - List details of EC2 Elastic IP addresses. - - This module was called C(ec2_eip_facts) before Ansible 2.9. The usage did not change. -version_added: "2.6" -author: "Brad Macpherson (@iiibrad)" -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and filter - value. See U(https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-addresses.html#options) - for possible filters. Filter names and values are case sensitive. - required: false - default: {} - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details or the AWS region, -# see the AWS Guide for details. - -# List all EIP addresses in the current region. -- ec2_eip_info: - register: regional_eip_addresses - -# List all EIP addresses for a VM. -- ec2_eip_info: - filters: - instance-id: i-123456789 - register: my_vm_eips - -- debug: msg="{{ my_vm_eips.addresses | json_query(\"[?private_ip_address=='10.0.0.5']\") }}" - -# List all EIP addresses for several VMs. -- ec2_eip_info: - filters: - instance-id: - - i-123456789 - - i-987654321 - register: my_vms_eips - -# List all EIP addresses using the 'Name' tag as a filter. -- ec2_eip_info: - filters: - tag:Name: www.example.com - register: my_vms_eips - -# List all EIP addresses using the Allocation-id as a filter -- ec2_eip_info: - filters: - allocation-id: eipalloc-64de1b01 - register: my_vms_eips - -# Set the variable eip_alloc to the value of the first allocation_id -# and set the variable my_pub_ip to the value of the first public_ip -- set_fact: - eip_alloc: my_vms_eips.addresses[0].allocation_id - my_pub_ip: my_vms_eips.addresses[0].public_ip - -''' - - -RETURN = ''' -addresses: - description: Properties of all Elastic IP addresses matching the provided filters. Each element is a dict with all the information related to an EIP. - returned: on success - type: list - sample: [{ - "allocation_id": "eipalloc-64de1b01", - "association_id": "eipassoc-0fe9ce90d6e983e97", - "domain": "vpc", - "instance_id": "i-01020cfeb25b0c84f", - "network_interface_id": "eni-02fdeadfd4beef9323b", - "network_interface_owner_id": "0123456789", - "private_ip_address": "10.0.0.1", - "public_ip": "54.81.104.1", - "tags": { - "Name": "test-vm-54.81.104.1" - } - }] - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict) -try: - from botocore.exceptions import (BotoCoreError, ClientError) -except ImportError: - pass # caught by imported AnsibleAWSModule - - -def get_eips_details(module): - connection = module.client('ec2') - filters = module.params.get("filters") - try: - response = connection.describe_addresses( - Filters=ansible_dict_to_boto3_filter_list(filters) - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws( - e, - msg="Error retrieving EIPs") - - addresses = camel_dict_to_snake_dict(response)['addresses'] - for address in addresses: - if 'tags' in address: - address['tags'] = boto3_tag_list_to_ansible_dict(address['tags']) - return addresses - - -def main(): - module = AnsibleAWSModule( - argument_spec=dict( - filters=dict(type='dict', default={}) - ), - supports_check_mode=True - ) - if module._module._name == 'ec2_eip_facts': - module._module.deprecate("The 'ec2_eip_facts' module has been renamed to 'ec2_eip_info'", version='2.13') - - module.exit_json(changed=False, addresses=get_eips_details(module)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_elb.py b/lib/ansible/modules/cloud/amazon/ec2_elb.py deleted file mode 100644 index 01cb34c038..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_elb.py +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_elb -short_description: De-registers or registers instances from EC2 ELBs -description: - - This module de-registers or registers an AWS EC2 instance from the ELBs - that it belongs to. - - Returns fact "ec2_elbs" which is a list of elbs attached to the instance - if state=absent is passed as an argument. - - Will be marked changed when called only if there are ELBs found to operate on. -version_added: "1.2" -author: "John Jarvis (@jarv)" -options: - state: - description: - - register or deregister the instance - required: true - choices: ['present', 'absent'] - type: str - instance_id: - description: - - EC2 Instance ID - required: true - type: str - ec2_elbs: - description: - - List of ELB names, required for registration. The ec2_elbs fact should be used if there was a previous de-register. - type: list - enable_availability_zone: - description: - - Whether to enable the availability zone of the instance on the target ELB if the availability zone has not already - been enabled. If set to no, the task will fail if the availability zone is not enabled on the ELB. - type: bool - default: 'yes' - wait: - description: - - Wait for instance registration or deregistration to complete successfully before returning. - type: bool - default: 'yes' - wait_timeout: - description: - - Number of seconds to wait for an instance to change state. If 0 then this module may return an error if a transient error occurs. - If non-zero then any transient errors are ignored until the timeout is reached. Ignored when wait=no. - default: 0 - version_added: "1.6" - type: int -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# basic pre_task and post_task example -pre_tasks: - - name: Gathering ec2 facts - action: ec2_facts - - name: Instance De-register - local_action: - module: ec2_elb - instance_id: "{{ ansible_ec2_instance_id }}" - state: absent -roles: - - myrole -post_tasks: - - name: Instance Register - local_action: - module: ec2_elb - instance_id: "{{ ansible_ec2_instance_id }}" - ec2_elbs: "{{ item }}" - state: present - loop: "{{ ec2_elbs }}" -""" - -import time - -try: - import boto - import boto.ec2 - import boto.ec2.autoscale - import boto.ec2.elb - from boto.regioninfo import RegionInfo - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (AnsibleAWSError, HAS_BOTO, connect_to_aws, ec2_argument_spec, - get_aws_connection_info) - - -class ElbManager: - """Handles EC2 instance ELB registration and de-registration""" - - def __init__(self, module, instance_id=None, ec2_elbs=None, - region=None, **aws_connect_params): - self.module = module - self.instance_id = instance_id - self.region = region - self.aws_connect_params = aws_connect_params - self.lbs = self._get_instance_lbs(ec2_elbs) - self.changed = False - - def deregister(self, wait, timeout): - """De-register the instance from all ELBs and wait for the ELB - to report it out-of-service""" - - for lb in self.lbs: - initial_state = self._get_instance_health(lb) - if initial_state is None: - # Instance isn't registered with this load - # balancer. Ignore it and try the next one. - continue - - # The instance is not associated with any load balancer so nothing to do - if not self._get_instance_lbs(): - return - - lb.deregister_instances([self.instance_id]) - - # The ELB is changing state in some way. Either an instance that's - # InService is moving to OutOfService, or an instance that's - # already OutOfService is being deregistered. - self.changed = True - - if wait: - self._await_elb_instance_state(lb, 'OutOfService', initial_state, timeout) - - def register(self, wait, enable_availability_zone, timeout): - """Register the instance for all ELBs and wait for the ELB - to report the instance in-service""" - for lb in self.lbs: - initial_state = self._get_instance_health(lb) - - if enable_availability_zone: - self._enable_availailability_zone(lb) - - lb.register_instances([self.instance_id]) - - if wait: - self._await_elb_instance_state(lb, 'InService', initial_state, timeout) - else: - # We cannot assume no change was made if we don't wait - # to find out - self.changed = True - - def exists(self, lbtest): - """ Verify that the named ELB actually exists """ - - found = False - for lb in self.lbs: - if lb.name == lbtest: - found = True - break - return found - - def _enable_availailability_zone(self, lb): - """Enable the current instance's availability zone in the provided lb. - Returns True if the zone was enabled or False if no change was made. - lb: load balancer""" - instance = self._get_instance() - if instance.placement in lb.availability_zones: - return False - - lb.enable_zones(zones=instance.placement) - - # If successful, the new zone will have been added to - # lb.availability_zones - return instance.placement in lb.availability_zones - - def _await_elb_instance_state(self, lb, awaited_state, initial_state, timeout): - """Wait for an ELB to change state - lb: load balancer - awaited_state : state to poll for (string)""" - - wait_timeout = time.time() + timeout - while True: - instance_state = self._get_instance_health(lb) - - if not instance_state: - msg = ("The instance %s could not be put in service on %s." - " Reason: Invalid Instance") - self.module.fail_json(msg=msg % (self.instance_id, lb)) - - if instance_state.state == awaited_state: - # Check the current state against the initial state, and only set - # changed if they are different. - if (initial_state is None) or (instance_state.state != initial_state.state): - self.changed = True - break - elif self._is_instance_state_pending(instance_state): - # If it's pending, we'll skip further checks and continue waiting - pass - elif (awaited_state == 'InService' - and instance_state.reason_code == "Instance" - and time.time() >= wait_timeout): - # If the reason_code for the instance being out of service is - # "Instance" this indicates a failure state, e.g. the instance - # has failed a health check or the ELB does not have the - # instance's availability zone enabled. The exact reason why is - # described in InstantState.description. - msg = ("The instance %s could not be put in service on %s." - " Reason: %s") - self.module.fail_json(msg=msg % (self.instance_id, - lb, - instance_state.description)) - time.sleep(1) - - def _is_instance_state_pending(self, instance_state): - """ - Determines whether the instance_state is "pending", meaning there is - an operation under way to bring it in service. - """ - # This is messy, because AWS provides no way to distinguish between - # an instance that is is OutOfService because it's pending vs. OutOfService - # because it's failing health checks. So we're forced to analyze the - # description, which is likely to be brittle. - return (instance_state and 'pending' in instance_state.description) - - def _get_instance_health(self, lb): - """ - Check instance health, should return status object or None under - certain error conditions. - """ - try: - status = lb.get_instance_health([self.instance_id])[0] - except boto.exception.BotoServerError as e: - if e.error_code == 'InvalidInstance': - return None - else: - raise - return status - - def _get_instance_lbs(self, ec2_elbs=None): - """Returns a list of ELBs attached to self.instance_id - ec2_elbs: an optional list of elb names that will be used - for elb lookup instead of returning what elbs - are attached to self.instance_id""" - - if not ec2_elbs: - ec2_elbs = self._get_auto_scaling_group_lbs() - - try: - elb = connect_to_aws(boto.ec2.elb, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - elbs = [] - marker = None - while True: - try: - newelbs = elb.get_all_load_balancers(marker=marker) - marker = newelbs.next_marker - elbs.extend(newelbs) - if not marker: - break - except TypeError: - # Older version of boto do not allow for params - elbs = elb.get_all_load_balancers() - break - - if ec2_elbs: - lbs = sorted(lb for lb in elbs if lb.name in ec2_elbs) - else: - lbs = [] - for lb in elbs: - for info in lb.instances: - if self.instance_id == info.id: - lbs.append(lb) - return lbs - - def _get_auto_scaling_group_lbs(self): - """Returns a list of ELBs associated with self.instance_id - indirectly through its auto scaling group membership""" - - try: - asg = connect_to_aws(boto.ec2.autoscale, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - asg_instances = asg.get_all_autoscaling_instances([self.instance_id]) - if len(asg_instances) > 1: - self.module.fail_json(msg="Illegal state, expected one auto scaling group instance.") - - if not asg_instances: - asg_elbs = [] - else: - asg_name = asg_instances[0].group_name - - asgs = asg.get_all_groups([asg_name]) - if len(asg_instances) != 1: - self.module.fail_json(msg="Illegal state, expected one auto scaling group.") - - asg_elbs = asgs[0].load_balancers - - return asg_elbs - - def _get_instance(self): - """Returns a boto.ec2.InstanceObject for self.instance_id""" - try: - ec2 = connect_to_aws(boto.ec2, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - return ec2.get_only_instances(instance_ids=[self.instance_id])[0] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state={'required': True, 'choices': ['present', 'absent']}, - instance_id={'required': True}, - ec2_elbs={'default': None, 'required': False, 'type': 'list'}, - enable_availability_zone={'default': True, 'required': False, 'type': 'bool'}, - wait={'required': False, 'default': True, 'type': 'bool'}, - wait_timeout={'required': False, 'default': 0, 'type': 'int'} - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - - ec2_elbs = module.params['ec2_elbs'] - wait = module.params['wait'] - enable_availability_zone = module.params['enable_availability_zone'] - timeout = module.params['wait_timeout'] - - if module.params['state'] == 'present' and 'ec2_elbs' not in module.params: - module.fail_json(msg="ELBs are required for registration") - - instance_id = module.params['instance_id'] - elb_man = ElbManager(module, instance_id, ec2_elbs, region=region, **aws_connect_params) - - if ec2_elbs is not None: - for elb in ec2_elbs: - if not elb_man.exists(elb): - msg = "ELB %s does not exist" % elb - module.fail_json(msg=msg) - - if not module.check_mode: - if module.params['state'] == 'present': - elb_man.register(wait, enable_availability_zone, timeout) - elif module.params['state'] == 'absent': - elb_man.deregister(wait, timeout) - - ansible_facts = {'ec2_elbs': [lb.name for lb in elb_man.lbs]} - ec2_facts_result = dict(changed=elb_man.changed, ansible_facts=ansible_facts) - - module.exit_json(**ec2_facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_elb_info.py b/lib/ansible/modules/cloud/amazon/ec2_elb_info.py deleted file mode 100644 index 9a48cdfaaa..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_elb_info.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/python -# -# This is a free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This Ansible library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_elb_info -short_description: Gather information about EC2 Elastic Load Balancers in AWS -description: - - Gather information about EC2 Elastic Load Balancers in AWS - - This module was called C(ec2_elb_facts) before Ansible 2.9. The usage did not change. -version_added: "2.0" -author: - - "Michael Schultz (@mjschultz)" - - "Fernando Jose Pando (@nand0p)" -options: - names: - description: - - List of ELB names to gather information about. Pass this option to gather information about a set of ELBs, otherwise, all ELBs are returned. - type: list -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Output format tries to match ec2_elb_lb module input parameters - -# Gather information about all ELBs -- action: - module: ec2_elb_info - register: elb_info - -- action: - module: debug - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" - -# Gather information about a particular ELB -- action: - module: ec2_elb_info - names: frontend-prod-elb - register: elb_info - -- action: - module: debug - msg: "{{ elb_info.elbs.0.dns_name }}" - -# Gather information about a set of ELBs -- action: - module: ec2_elb_info - names: - - frontend-prod-elb - - backend-prod-elb - register: elb_info - -- action: - module: debug - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" - -''' - -import traceback - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ( - AWSRetry, - connect_to_aws, - ec2_argument_spec, - get_aws_connection_info, -) - -try: - import boto.ec2.elb - from boto.ec2.tag import Tag - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - - -class ElbInformation(object): - """Handles ELB information.""" - - def __init__(self, - module, - names, - region, - **aws_connect_params): - - self.module = module - self.names = names - self.region = region - self.aws_connect_params = aws_connect_params - self.connection = self._get_elb_connection() - - def _get_tags(self, elbname): - params = {'LoadBalancerNames.member.1': elbname} - elb_tags = self.connection.get_list('DescribeTags', params, [('member', Tag)]) - return dict((tag.Key, tag.Value) for tag in elb_tags if hasattr(tag, 'Key')) - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) - def _get_elb_connection(self): - return connect_to_aws(boto.ec2.elb, self.region, **self.aws_connect_params) - - def _get_elb_listeners(self, listeners): - listener_list = [] - - for listener in listeners: - listener_dict = { - 'load_balancer_port': listener[0], - 'instance_port': listener[1], - 'protocol': listener[2], - 'instance_protocol': listener[3] - } - - try: - ssl_certificate_id = listener[4] - except IndexError: - pass - else: - if ssl_certificate_id: - listener_dict['ssl_certificate_id'] = ssl_certificate_id - - listener_list.append(listener_dict) - - return listener_list - - def _get_health_check(self, health_check): - protocol, port_path = health_check.target.split(':') - try: - port, path = port_path.split('/', 1) - path = '/{0}'.format(path) - except ValueError: - port = port_path - path = None - - health_check_dict = { - 'ping_protocol': protocol.lower(), - 'ping_port': int(port), - 'response_timeout': health_check.timeout, - 'interval': health_check.interval, - 'unhealthy_threshold': health_check.unhealthy_threshold, - 'healthy_threshold': health_check.healthy_threshold, - } - - if path: - health_check_dict['ping_path'] = path - return health_check_dict - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) - def _get_elb_info(self, elb): - elb_info = { - 'name': elb.name, - 'zones': elb.availability_zones, - 'dns_name': elb.dns_name, - 'canonical_hosted_zone_name': elb.canonical_hosted_zone_name, - 'canonical_hosted_zone_name_id': elb.canonical_hosted_zone_name_id, - 'hosted_zone_name': elb.canonical_hosted_zone_name, - 'hosted_zone_id': elb.canonical_hosted_zone_name_id, - 'instances': [instance.id for instance in elb.instances], - 'listeners': self._get_elb_listeners(elb.listeners), - 'scheme': elb.scheme, - 'security_groups': elb.security_groups, - 'health_check': self._get_health_check(elb.health_check), - 'subnets': elb.subnets, - 'instances_inservice': [], - 'instances_inservice_count': 0, - 'instances_outofservice': [], - 'instances_outofservice_count': 0, - 'instances_inservice_percent': 0.0, - 'tags': self._get_tags(elb.name) - } - - if elb.vpc_id: - elb_info['vpc_id'] = elb.vpc_id - - if elb.instances: - instance_health = self.connection.describe_instance_health(elb.name) - elb_info['instances_inservice'] = [inst.instance_id for inst in instance_health if inst.state == 'InService'] - elb_info['instances_inservice_count'] = len(elb_info['instances_inservice']) - elb_info['instances_outofservice'] = [inst.instance_id for inst in instance_health if inst.state == 'OutOfService'] - elb_info['instances_outofservice_count'] = len(elb_info['instances_outofservice']) - try: - elb_info['instances_inservice_percent'] = ( - float(elb_info['instances_inservice_count']) / - float(elb_info['instances_inservice_count'] + elb_info['instances_outofservice_count']) - ) * 100. - except ZeroDivisionError: - elb_info['instances_inservice_percent'] = 0. - return elb_info - - def list_elbs(self): - elb_array, token = [], None - get_elb_with_backoff = AWSRetry.backoff(tries=5, delay=5, backoff=2.0)(self.connection.get_all_load_balancers) - while True: - all_elbs = get_elb_with_backoff(marker=token) - token = all_elbs.next_marker - - if all_elbs: - if self.names: - for existing_lb in all_elbs: - if existing_lb.name in self.names: - elb_array.append(existing_lb) - else: - elb_array.extend(all_elbs) - else: - break - - if token is None: - break - - return list(map(self._get_elb_info, elb_array)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - names={'default': [], 'type': 'list'} - ) - ) - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_elb_facts': - module.deprecate("The 'ec2_elb_facts' module has been renamed to 'ec2_elb_info'", version='2.13') - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - try: - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg="region must be specified") - - names = module.params['names'] - elb_information = ElbInformation( - module, names, region, **aws_connect_params) - - ec2_info_result = dict(changed=False, - elbs=elb_information.list_elbs()) - - except BotoServerError as err: - module.fail_json(msg="{0}: {1}".format(err.error_code, err.error_message), - exception=traceback.format_exc()) - - module.exit_json(**ec2_info_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_instance.py b/lib/ansible/modules/cloud/amazon/ec2_instance.py deleted file mode 100644 index 7a587fb941..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_instance.py +++ /dev/null @@ -1,1805 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ec2_instance -short_description: Create & manage EC2 instances -description: - - Create and manage AWS EC2 instances. - - > - Note: This module does not support creating - L(EC2 Spot instances,https://aws.amazon.com/ec2/spot/). The M(ec2) module - can create and manage spot instances. -version_added: "2.5" -author: - - Ryan Scott Brown (@ryansb) -requirements: [ "boto3", "botocore" ] -options: - instance_ids: - description: - - If you specify one or more instance IDs, only instances that have the specified IDs are returned. - type: list - state: - description: - - Goal state for the instances. - choices: [present, terminated, running, started, stopped, restarted, rebooted, absent] - default: present - type: str - wait: - description: - - Whether or not to wait for the desired state (use wait_timeout to customize this). - default: true - type: bool - wait_timeout: - description: - - How long to wait (in seconds) for the instance to finish booting/terminating. - default: 600 - type: int - instance_type: - description: - - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) - Only required when instance is not already present. - default: t2.micro - type: str - user_data: - description: - - Opaque blob of data which is made available to the ec2 instance - type: str - tower_callback: - description: - - Preconfigured user-data to enable an instance to perform a Tower callback (Linux only). - - Mutually exclusive with I(user_data). - - For Windows instances, to enable remote access via Ansible set I(tower_callback.windows) to true, and optionally set an admin password. - - If using 'windows' and 'set_password', callback to Tower will not be performed but the instance will be ready to receive winrm connections from Ansible. - type: dict - suboptions: - tower_address: - description: - - IP address or DNS name of Tower server. Must be accessible via this address from the VPC that this instance will be launched in. - type: str - job_template_id: - description: - - Either the integer ID of the Tower Job Template, or the name (name supported only for Tower 3.2+). - type: str - host_config_key: - description: - - Host configuration secret key generated by the Tower job template. - type: str - tags: - description: - - A hash/dictionary of tags to add to the new instance or to add/remove from an existing one. - type: dict - purge_tags: - description: - - Delete any tags not specified in the task that are on the instance. - This means you have to specify all the desired tags on each task affecting an instance. - default: false - type: bool - image: - description: - - An image to use for the instance. The M(ec2_ami_info) module may be used to retrieve images. - One of I(image) or I(image_id) are required when instance is not already present. - type: dict - suboptions: - id: - description: - - The AMI ID. - type: str - ramdisk: - description: - - Overrides the AMI's default ramdisk ID. - type: str - kernel: - description: - - a string AKI to override the AMI kernel. - image_id: - description: - - I(ami) ID to use for the instance. One of I(image) or I(image_id) are required when instance is not already present. - - This is an alias for I(image.id). - type: str - security_groups: - description: - - A list of security group IDs or names (strings). Mutually exclusive with I(security_group). - type: list - security_group: - description: - - A security group ID or name. Mutually exclusive with I(security_groups). - type: str - name: - description: - - The Name tag for the instance. - type: str - vpc_subnet_id: - description: - - The subnet ID in which to launch the instance (VPC) - If none is provided, ec2_instance will chose the default zone of the default VPC. - aliases: ['subnet_id'] - type: str - network: - description: - - Either a dictionary containing the key 'interfaces' corresponding to a list of network interface IDs or - containing specifications for a single network interface. - - Use the ec2_eni module to create ENIs with special settings. - type: dict - suboptions: - interfaces: - description: - - a list of ENI IDs (strings) or a list of objects containing the key I(id). - type: list - assign_public_ip: - description: - - when true assigns a public IP address to the interface - type: bool - private_ip_address: - description: - - an IPv4 address to assign to the interface - type: str - ipv6_addresses: - description: - - a list of IPv6 addresses to assign to the network interface - type: list - source_dest_check: - description: - - controls whether source/destination checking is enabled on the interface - type: bool - description: - description: - - a description for the network interface - type: str - private_ip_addresses: - description: - - a list of IPv4 addresses to assign to the network interface - type: list - subnet_id: - description: - - the subnet to connect the network interface to - type: str - delete_on_termination: - description: - - Delete the interface when the instance it is attached to is - terminated. - type: bool - device_index: - description: - - The index of the interface to modify - type: int - groups: - description: - - a list of security group IDs to attach to the interface - type: list - volumes: - description: - - A list of block device mappings, by default this will always use the AMI root device so the volumes option is primarily for adding more storage. - - A mapping contains the (optional) keys device_name, virtual_name, ebs.volume_type, ebs.volume_size, ebs.kms_key_id, - ebs.iops, and ebs.delete_on_termination. - - For more information about each parameter, see U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html). - type: list - launch_template: - description: - - The EC2 launch template to base instance configuration on. - type: dict - suboptions: - id: - description: - - the ID of the launch template (optional if name is specified). - type: str - name: - description: - - the pretty name of the launch template (optional if id is specified). - type: str - version: - description: - - the specific version of the launch template to use. If unspecified, the template default is chosen. - key_name: - description: - - Name of the SSH access key to assign to the instance - must exist in the region the instance is created. - type: str - availability_zone: - description: - - Specify an availability zone to use the default subnet it. Useful if not specifying the I(vpc_subnet_id) parameter. - - If no subnet, ENI, or availability zone is provided, the default subnet in the default VPC will be used in the first AZ (alphabetically sorted). - type: str - instance_initiated_shutdown_behavior: - description: - - Whether to stop or terminate an instance upon shutdown. - choices: ['stop', 'terminate'] - type: str - tenancy: - description: - - What type of tenancy to allow an instance to use. Default is shared tenancy. Dedicated tenancy will incur additional charges. - choices: ['dedicated', 'default'] - type: str - termination_protection: - description: - - Whether to enable termination protection. - This module will not terminate an instance with termination protection active, it must be turned off first. - type: bool - cpu_credit_specification: - description: - - For T series instances, choose whether to allow increased charges to buy CPU credits if the default pool is depleted. - - Choose I(unlimited) to enable buying additional CPU credits. - choices: ['unlimited', 'standard'] - type: str - cpu_options: - description: - - Reduce the number of vCPU exposed to the instance. - - Those parameters can only be set at instance launch. The two suboptions threads_per_core and core_count are mandatory. - - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html) for combinations available. - - Requires botocore >= 1.10.16 - version_added: 2.7 - type: dict - suboptions: - threads_per_core: - description: - - Select the number of threads per core to enable. Disable or Enable Intel HT. - choices: [1, 2] - required: true - type: int - core_count: - description: - - Set the number of core to enable. - required: true - type: int - detailed_monitoring: - description: - - Whether to allow detailed cloudwatch metrics to be collected, enabling more detailed alerting. - type: bool - ebs_optimized: - description: - - Whether instance is should use optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html). - type: bool - filters: - description: - - A dict of filters to apply when deciding whether existing instances match and should be altered. Each dict item - consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). - for possible filters. Filter names and values are case sensitive. - - By default, instances are filtered for counting by their "Name" tag, base AMI, state (running, by default), and - subnet ID. Any queryable filter can be used. Good candidates are specific tags, SSH keys, or security groups. - type: dict - instance_role: - description: - - The ARN or name of an EC2-enabled instance role to be used. If a name is not provided in arn format - then the ListInstanceProfiles permission must also be granted. - U(https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListInstanceProfiles.html) If no full ARN is provided, - the role with a matching name will be used from the active AWS account. - type: str - placement_group: - description: - - The placement group that needs to be assigned to the instance - version_added: 2.8 - type: str - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Terminate every running instance in a region. Use with EXTREME caution. -- ec2_instance: - state: absent - filters: - instance-state-name: running - -# restart a particular instance by its ID -- ec2_instance: - state: restarted - instance_ids: - - i-12345678 - -# start an instance with a public IP address -- ec2_instance: - name: "public-compute-instance" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - instance_type: c5.large - security_group: default - network: - assign_public_ip: true - image_id: ami-123456 - tags: - Environment: Testing - -# start an instance and Add EBS -- ec2_instance: - name: "public-withebs-instance" - vpc_subnet_id: subnet-5ca1ab1e - instance_type: t2.micro - key_name: "prod-ssh-key" - security_group: default - volumes: - - device_name: /dev/sda1 - ebs: - volume_size: 16 - delete_on_termination: true - -# start an instance with a cpu_options -- ec2_instance: - name: "public-cpuoption-instance" - vpc_subnet_id: subnet-5ca1ab1e - tags: - Environment: Testing - instance_type: c4.large - volumes: - - device_name: /dev/sda1 - ebs: - delete_on_termination: true - cpu_options: - core_count: 1 - threads_per_core: 1 - -# start an instance and have it begin a Tower callback on boot -- ec2_instance: - name: "tower-callback-test" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - security_group: default - tower_callback: - # IP or hostname of tower server - tower_address: 1.2.3.4 - job_template_id: 876 - host_config_key: '[secret config key goes here]' - network: - assign_public_ip: true - image_id: ami-123456 - cpu_credit_specification: unlimited - tags: - SomeThing: "A value" - -# start an instance with ENI (An existing ENI ID is required) -- ec2_instance: - name: "public-eni-instance" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - network: - interfaces: - - id: "eni-12345" - tags: - Env: "eni_on" - volumes: - - device_name: /dev/sda1 - ebs: - delete_on_termination: true - instance_type: t2.micro - image_id: ami-123456 - -# add second ENI interface -- ec2_instance: - name: "public-eni-instance" - network: - interfaces: - - id: "eni-12345" - - id: "eni-67890" - image_id: ami-123456 - tags: - Env: "eni_on" - instance_type: t2.micro -''' - -RETURN = ''' -instances: - description: a list of ec2 instances - returned: when wait == true - type: complex - contains: - ami_launch_index: - description: The AMI launch index, which can be used to find this instance in the launch group. - returned: always - type: int - sample: 0 - architecture: - description: The architecture of the image - returned: always - type: str - sample: x86_64 - block_device_mappings: - description: Any block device mapping entries for the instance. - returned: always - type: complex - contains: - device_name: - description: The device name exposed to the instance (for example, /dev/sdh or xvdh). - returned: always - type: str - sample: /dev/sdh - ebs: - description: Parameters used to automatically set up EBS volumes when the instance is launched. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - delete_on_termination: - description: Indicates whether the volume is deleted on instance termination. - returned: always - type: bool - sample: true - status: - description: The attachment state. - returned: always - type: str - sample: attached - volume_id: - description: The ID of the EBS volume - returned: always - type: str - sample: vol-12345678 - client_token: - description: The idempotency token you provided when you launched the instance, if applicable. - returned: always - type: str - sample: mytoken - ebs_optimized: - description: Indicates whether the instance is optimized for EBS I/O. - returned: always - type: bool - sample: false - hypervisor: - description: The hypervisor type of the instance. - returned: always - type: str - sample: xen - iam_instance_profile: - description: The IAM instance profile associated with the instance, if applicable. - returned: always - type: complex - contains: - arn: - description: The Amazon Resource Name (ARN) of the instance profile. - returned: always - type: str - sample: "arn:aws:iam::000012345678:instance-profile/myprofile" - id: - description: The ID of the instance profile - returned: always - type: str - sample: JFJ397FDG400FG9FD1N - image_id: - description: The ID of the AMI used to launch the instance. - returned: always - type: str - sample: ami-0011223344 - instance_id: - description: The ID of the instance. - returned: always - type: str - sample: i-012345678 - instance_type: - description: The instance type size of the running instance. - returned: always - type: str - sample: t2.micro - key_name: - description: The name of the key pair, if this instance was launched with an associated key pair. - returned: always - type: str - sample: my-key - launch_time: - description: The time the instance was launched. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - monitoring: - description: The monitoring for the instance. - returned: always - type: complex - contains: - state: - description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. - returned: always - type: str - sample: disabled - network_interfaces: - description: One or more network interfaces for the instance. - returned: always - type: complex - contains: - association: - description: The association information for an Elastic IPv4 associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - attachment: - description: The network interface attachment. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - attachment_id: - description: The ID of the network interface attachment. - returned: always - type: str - sample: eni-attach-3aff3f - delete_on_termination: - description: Indicates whether the network interface is deleted when the instance is terminated. - returned: always - type: bool - sample: true - device_index: - description: The index of the device on the instance for the network interface attachment. - returned: always - type: int - sample: 0 - status: - description: The attachment state. - returned: always - type: str - sample: attached - description: - description: The description. - returned: always - type: str - sample: My interface - groups: - description: One or more security groups. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-abcdef12 - group_name: - description: The name of the security group. - returned: always - type: str - sample: mygroup - ipv6_addresses: - description: One or more IPv6 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - ipv6_address: - description: The IPv6 address. - returned: always - type: str - sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - mac_address: - description: The MAC address. - returned: always - type: str - sample: "00:11:22:33:44:55" - network_interface_id: - description: The ID of the network interface. - returned: always - type: str - sample: eni-01234567 - owner_id: - description: The AWS account ID of the owner of the network interface. - returned: always - type: str - sample: 01234567890 - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - private_ip_addresses: - description: The private IPv4 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - association: - description: The association information for an Elastic IP address (IPv4) associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - primary: - description: Indicates whether this IPv4 address is the primary private IP address of the network interface. - returned: always - type: bool - sample: true - private_ip_address: - description: The private IPv4 address of the network interface. - returned: always - type: str - sample: 10.0.0.1 - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - status: - description: The status of the network interface. - returned: always - type: str - sample: in-use - subnet_id: - description: The ID of the subnet for the network interface. - returned: always - type: str - sample: subnet-0123456 - vpc_id: - description: The ID of the VPC for the network interface. - returned: always - type: str - sample: vpc-0123456 - placement: - description: The location where the instance launched, if applicable. - returned: always - type: complex - contains: - availability_zone: - description: The Availability Zone of the instance. - returned: always - type: str - sample: ap-southeast-2a - group_name: - description: The name of the placement group the instance is in (for cluster compute instances). - returned: always - type: str - sample: "" - tenancy: - description: The tenancy of the instance (if the instance is running in a VPC). - returned: always - type: str - sample: default - private_dns_name: - description: The private DNS name. - returned: always - type: str - sample: ip-10-0-0-1.ap-southeast-2.compute.internal - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - product_codes: - description: One or more product codes. - returned: always - type: list - elements: dict - contains: - product_code_id: - description: The product code. - returned: always - type: str - sample: aw0evgkw8ef3n2498gndfgasdfsd5cce - product_code_type: - description: The type of product code. - returned: always - type: str - sample: marketplace - public_dns_name: - description: The public DNS name assigned to the instance. - returned: always - type: str - sample: - public_ip_address: - description: The public IPv4 address assigned to the instance - returned: always - type: str - sample: 52.0.0.1 - root_device_name: - description: The device name of the root device - returned: always - type: str - sample: /dev/sda1 - root_device_type: - description: The type of root device used by the AMI. - returned: always - type: str - sample: ebs - security_groups: - description: One or more security groups for the instance. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-0123456 - group_name: - description: The name of the security group. - returned: always - type: str - sample: my-security-group - network.source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - state: - description: The current state of the instance. - returned: always - type: complex - contains: - code: - description: The low byte represents the state. - returned: always - type: int - sample: 16 - name: - description: The name of the state. - returned: always - type: str - sample: running - state_transition_reason: - description: The reason for the most recent state transition. - returned: always - type: str - sample: - subnet_id: - description: The ID of the subnet in which the instance is running. - returned: always - type: str - sample: subnet-00abcdef - tags: - description: Any tags assigned to the instance. - returned: always - type: dict - sample: - virtualization_type: - description: The type of virtualization of the AMI. - returned: always - type: str - sample: hvm - vpc_id: - description: The ID of the VPC the instance is in. - returned: always - type: dict - sample: vpc-0011223344 -''' - -import re -import uuid -import string -import textwrap -import time -from collections import namedtuple - -try: - import boto3 - import botocore.exceptions -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.six import text_type, string_types -from ansible.module_utils.six.moves.urllib import parse as urlparse -from ansible.module_utils._text import to_bytes, to_native -import ansible.module_utils.ec2 as ec2_utils -from ansible.module_utils.ec2 import (AWSRetry, - ansible_dict_to_boto3_filter_list, - compare_aws_tags, - boto3_tag_list_to_ansible_dict, - ansible_dict_to_boto3_tag_list, - camel_dict_to_snake_dict) - -from ansible.module_utils.aws.core import AnsibleAWSModule - -module = None - - -def tower_callback_script(tower_conf, windows=False, passwd=None): - script_url = 'https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1' - if windows and passwd is not None: - script_tpl = """<powershell> - $admin = [adsi]("WinNT://./administrator, user") - $admin.PSBase.Invoke("SetPassword", "{PASS}") - Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) - </powershell> - """ - return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) - elif windows and passwd is None: - script_tpl = """<powershell> - $admin = [adsi]("WinNT://./administrator, user") - Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) - </powershell> - """ - return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) - elif not windows: - for p in ['tower_address', 'job_template_id', 'host_config_key']: - if p not in tower_conf: - module.fail_json(msg="Incomplete tower_callback configuration. tower_callback.{0} not set.".format(p)) - - if isinstance(tower_conf['job_template_id'], string_types): - tower_conf['job_template_id'] = urlparse.quote(tower_conf['job_template_id']) - tpl = string.Template(textwrap.dedent("""#!/bin/bash - set -x - - retry_attempts=10 - attempt=0 - while [[ $attempt -lt $retry_attempts ]] - do - status_code=`curl --max-time 10 -v -k -s -i \ - --data "host_config_key=${host_config_key}" \ - 'https://${tower_address}/api/v2/job_templates/${template_id}/callback/' \ - | head -n 1 \ - | awk '{print $2}'` - if [[ $status_code == 404 ]] - then - status_code=`curl --max-time 10 -v -k -s -i \ - --data "host_config_key=${host_config_key}" \ - 'https://${tower_address}/api/v1/job_templates/${template_id}/callback/' \ - | head -n 1 \ - | awk '{print $2}'` - # fall back to using V1 API for Tower 3.1 and below, since v2 API will always 404 - fi - if [[ $status_code == 201 ]] - then - exit 0 - fi - attempt=$(( attempt + 1 )) - echo "$${status_code} received... retrying in 1 minute. (Attempt $${attempt})" - sleep 60 - done - exit 1 - """)) - return tpl.safe_substitute(tower_address=tower_conf['tower_address'], - template_id=tower_conf['job_template_id'], - host_config_key=tower_conf['host_config_key']) - raise NotImplementedError("Only windows with remote-prep or non-windows with tower job callback supported so far.") - - -@AWSRetry.jittered_backoff() -def manage_tags(match, new_tags, purge_tags, ec2): - changed = False - old_tags = boto3_tag_list_to_ansible_dict(match['Tags']) - tags_to_set, tags_to_delete = compare_aws_tags( - old_tags, new_tags, - purge_tags=purge_tags, - ) - if tags_to_set: - ec2.create_tags( - Resources=[match['InstanceId']], - Tags=ansible_dict_to_boto3_tag_list(tags_to_set)) - changed |= True - if tags_to_delete: - delete_with_current_values = dict((k, old_tags.get(k)) for k in tags_to_delete) - ec2.delete_tags( - Resources=[match['InstanceId']], - Tags=ansible_dict_to_boto3_tag_list(delete_with_current_values)) - changed |= True - return changed - - -def build_volume_spec(params): - volumes = params.get('volumes') or [] - for volume in volumes: - if 'ebs' in volume: - for int_value in ['volume_size', 'iops']: - if int_value in volume['ebs']: - volume['ebs'][int_value] = int(volume['ebs'][int_value]) - return [ec2_utils.snake_dict_to_camel_dict(v, capitalize_first=True) for v in volumes] - - -def add_or_update_instance_profile(instance, desired_profile_name): - instance_profile_setting = instance.get('IamInstanceProfile') - if instance_profile_setting and desired_profile_name: - if desired_profile_name in (instance_profile_setting.get('Name'), instance_profile_setting.get('Arn')): - # great, the profile we asked for is what's there - return False - else: - desired_arn = determine_iam_role(desired_profile_name) - if instance_profile_setting.get('Arn') == desired_arn: - return False - # update association - ec2 = module.client('ec2') - try: - association = ec2.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}]) - except botocore.exceptions.ClientError as e: - # check for InvalidAssociationID.NotFound - module.fail_json_aws(e, "Could not find instance profile association") - try: - resp = ec2.replace_iam_instance_profile_association( - AssociationId=association['IamInstanceProfileAssociations'][0]['AssociationId'], - IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)} - ) - return True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, "Could not associate instance profile") - - if not instance_profile_setting and desired_profile_name: - # create association - ec2 = module.client('ec2') - try: - resp = ec2.associate_iam_instance_profile( - IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)}, - InstanceId=instance['InstanceId'] - ) - return True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, "Could not associate new instance profile") - - return False - - -def build_network_spec(params, ec2=None): - """ - Returns list of interfaces [complex] - Interface type: { - 'AssociatePublicIpAddress': True|False, - 'DeleteOnTermination': True|False, - 'Description': 'string', - 'DeviceIndex': 123, - 'Groups': [ - 'string', - ], - 'Ipv6AddressCount': 123, - 'Ipv6Addresses': [ - { - 'Ipv6Address': 'string' - }, - ], - 'NetworkInterfaceId': 'string', - 'PrivateIpAddress': 'string', - 'PrivateIpAddresses': [ - { - 'Primary': True|False, - 'PrivateIpAddress': 'string' - }, - ], - 'SecondaryPrivateIpAddressCount': 123, - 'SubnetId': 'string' - }, - """ - if ec2 is None: - ec2 = module.client('ec2') - - interfaces = [] - network = params.get('network') or {} - if not network.get('interfaces'): - # they only specified one interface - spec = { - 'DeviceIndex': 0, - } - if network.get('assign_public_ip') is not None: - spec['AssociatePublicIpAddress'] = network['assign_public_ip'] - - if params.get('vpc_subnet_id'): - spec['SubnetId'] = params['vpc_subnet_id'] - else: - default_vpc = get_default_vpc(ec2) - if default_vpc is None: - raise module.fail_json( - msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to create an instance") - else: - sub = get_default_subnet(ec2, default_vpc) - spec['SubnetId'] = sub['SubnetId'] - - if network.get('private_ip_address'): - spec['PrivateIpAddress'] = network['private_ip_address'] - - if params.get('security_group') or params.get('security_groups'): - groups = discover_security_groups( - group=params.get('security_group'), - groups=params.get('security_groups'), - subnet_id=spec['SubnetId'], - ec2=ec2 - ) - spec['Groups'] = [g['GroupId'] for g in groups] - if network.get('description') is not None: - spec['Description'] = network['description'] - # TODO more special snowflake network things - - return [spec] - - # handle list of `network.interfaces` options - for idx, interface_params in enumerate(network.get('interfaces', [])): - spec = { - 'DeviceIndex': idx, - } - - if isinstance(interface_params, string_types): - # naive case where user gave - # network_interfaces: [eni-1234, eni-4567, ....] - # put into normal data structure so we don't dupe code - interface_params = {'id': interface_params} - - if interface_params.get('id') is not None: - # if an ID is provided, we don't want to set any other parameters. - spec['NetworkInterfaceId'] = interface_params['id'] - interfaces.append(spec) - continue - - spec['DeleteOnTermination'] = interface_params.get('delete_on_termination', True) - - if interface_params.get('ipv6_addresses'): - spec['Ipv6Addresses'] = [{'Ipv6Address': a} for a in interface_params.get('ipv6_addresses', [])] - - if interface_params.get('private_ip_address'): - spec['PrivateIpAddress'] = interface_params.get('private_ip_address') - - if interface_params.get('description'): - spec['Description'] = interface_params.get('description') - - if interface_params.get('subnet_id', params.get('vpc_subnet_id')): - spec['SubnetId'] = interface_params.get('subnet_id', params.get('vpc_subnet_id')) - elif not spec.get('SubnetId') and not interface_params['id']: - # TODO grab a subnet from default VPC - raise ValueError('Failed to assign subnet to interface {0}'.format(interface_params)) - - interfaces.append(spec) - return interfaces - - -def warn_if_public_ip_assignment_changed(instance): - # This is a non-modifiable attribute. - assign_public_ip = (module.params.get('network') or {}).get('assign_public_ip') - if assign_public_ip is None: - return - - # Check that public ip assignment is the same and warn if not - public_dns_name = instance.get('PublicDnsName') - if (public_dns_name and not assign_public_ip) or (assign_public_ip and not public_dns_name): - module.warn( - "Unable to modify public ip assignment to {0} for instance {1}. " - "Whether or not to assign a public IP is determined during instance creation.".format( - assign_public_ip, instance['InstanceId'])) - - -def warn_if_cpu_options_changed(instance): - # This is a non-modifiable attribute. - cpu_options = module.params.get('cpu_options') - if cpu_options is None: - return - - # Check that the CpuOptions set are the same and warn if not - core_count_curr = instance['CpuOptions'].get('CoreCount') - core_count = cpu_options.get('core_count') - threads_per_core_curr = instance['CpuOptions'].get('ThreadsPerCore') - threads_per_core = cpu_options.get('threads_per_core') - if core_count_curr != core_count: - module.warn( - "Unable to modify core_count from {0} to {1}. " - "Assigning a number of core is determinted during instance creation".format( - core_count_curr, core_count)) - - if threads_per_core_curr != threads_per_core: - module.warn( - "Unable to modify threads_per_core from {0} to {1}. " - "Assigning a number of threads per core is determined during instance creation.".format( - threads_per_core_curr, threads_per_core)) - - -def discover_security_groups(group, groups, parent_vpc_id=None, subnet_id=None, ec2=None): - if ec2 is None: - ec2 = module.client('ec2') - - if subnet_id is not None: - try: - sub = ec2.describe_subnets(SubnetIds=[subnet_id]) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidGroup.NotFound': - module.fail_json( - "Could not find subnet {0} to associate security groups. Please check the vpc_subnet_id and security_groups parameters.".format( - subnet_id - ) - ) - module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id)) - parent_vpc_id = sub['Subnets'][0]['VpcId'] - - vpc = { - 'Name': 'vpc-id', - 'Values': [parent_vpc_id] - } - - # because filter lists are AND in the security groups API, - # make two separate requests for groups by ID and by name - id_filters = [vpc] - name_filters = [vpc] - - if group: - name_filters.append( - dict( - Name='group-name', - Values=[group] - ) - ) - if group.startswith('sg-'): - id_filters.append( - dict( - Name='group-id', - Values=[group] - ) - ) - if groups: - name_filters.append( - dict( - Name='group-name', - Values=groups - ) - ) - if [g for g in groups if g.startswith('sg-')]: - id_filters.append( - dict( - Name='group-id', - Values=[g for g in groups if g.startswith('sg-')] - ) - ) - - found_groups = [] - for f_set in (id_filters, name_filters): - if len(f_set) > 1: - found_groups.extend(ec2.get_paginator( - 'describe_security_groups' - ).paginate( - Filters=f_set - ).search('SecurityGroups[]')) - return list(dict((g['GroupId'], g) for g in found_groups).values()) - - -def build_top_level_options(params): - spec = {} - if params.get('image_id'): - spec['ImageId'] = params['image_id'] - elif isinstance(params.get('image'), dict): - image = params.get('image', {}) - spec['ImageId'] = image.get('id') - if 'ramdisk' in image: - spec['RamdiskId'] = image['ramdisk'] - if 'kernel' in image: - spec['KernelId'] = image['kernel'] - if not spec.get('ImageId') and not params.get('launch_template'): - module.fail_json(msg="You must include an image_id or image.id parameter to create an instance, or use a launch_template.") - - if params.get('key_name') is not None: - spec['KeyName'] = params.get('key_name') - if params.get('user_data') is not None: - spec['UserData'] = to_native(params.get('user_data')) - elif params.get('tower_callback') is not None: - spec['UserData'] = tower_callback_script( - tower_conf=params.get('tower_callback'), - windows=params.get('tower_callback').get('windows', False), - passwd=params.get('tower_callback').get('set_password'), - ) - - if params.get('launch_template') is not None: - spec['LaunchTemplate'] = {} - if not params.get('launch_template').get('id') or params.get('launch_template').get('name'): - module.fail_json(msg="Could not create instance with launch template. Either launch_template.name or launch_template.id parameters are required") - - if params.get('launch_template').get('id') is not None: - spec['LaunchTemplate']['LaunchTemplateId'] = params.get('launch_template').get('id') - if params.get('launch_template').get('name') is not None: - spec['LaunchTemplate']['LaunchTemplateName'] = params.get('launch_template').get('name') - if params.get('launch_template').get('version') is not None: - spec['LaunchTemplate']['Version'] = to_native(params.get('launch_template').get('version')) - - if params.get('detailed_monitoring', False): - spec['Monitoring'] = {'Enabled': True} - if params.get('cpu_credit_specification') is not None: - spec['CreditSpecification'] = {'CpuCredits': params.get('cpu_credit_specification')} - if params.get('tenancy') is not None: - spec['Placement'] = {'Tenancy': params.get('tenancy')} - if params.get('placement_group'): - if 'Placement' in spec: - spec['Placement']['GroupName'] = str(params.get('placement_group')) - else: - spec.setdefault('Placement', {'GroupName': str(params.get('placement_group'))}) - if params.get('ebs_optimized') is not None: - spec['EbsOptimized'] = params.get('ebs_optimized') - if params.get('instance_initiated_shutdown_behavior'): - spec['InstanceInitiatedShutdownBehavior'] = params.get('instance_initiated_shutdown_behavior') - if params.get('termination_protection') is not None: - spec['DisableApiTermination'] = params.get('termination_protection') - if params.get('cpu_options') is not None: - spec['CpuOptions'] = {} - spec['CpuOptions']['ThreadsPerCore'] = params.get('cpu_options').get('threads_per_core') - spec['CpuOptions']['CoreCount'] = params.get('cpu_options').get('core_count') - return spec - - -def build_instance_tags(params, propagate_tags_to_volumes=True): - tags = params.get('tags', {}) - if params.get('name') is not None: - if tags is None: - tags = {} - tags['Name'] = params.get('name') - return [ - { - 'ResourceType': 'volume', - 'Tags': ansible_dict_to_boto3_tag_list(tags), - }, - { - 'ResourceType': 'instance', - 'Tags': ansible_dict_to_boto3_tag_list(tags), - }, - ] - - -def build_run_instance_spec(params, ec2=None): - if ec2 is None: - ec2 = module.client('ec2') - - spec = dict( - ClientToken=uuid.uuid4().hex, - MaxCount=1, - MinCount=1, - ) - # network parameters - spec['NetworkInterfaces'] = build_network_spec(params, ec2) - spec['BlockDeviceMappings'] = build_volume_spec(params) - spec.update(**build_top_level_options(params)) - spec['TagSpecifications'] = build_instance_tags(params) - - # IAM profile - if params.get('instance_role'): - spec['IamInstanceProfile'] = dict(Arn=determine_iam_role(params.get('instance_role'))) - - spec['InstanceType'] = params['instance_type'] - return spec - - -def await_instances(ids, state='OK'): - if not module.params.get('wait', True): - # the user asked not to wait for anything - return - - if module.check_mode: - # In check mode, there is no change even if you wait. - return - - state_opts = { - 'OK': 'instance_status_ok', - 'STOPPED': 'instance_stopped', - 'TERMINATED': 'instance_terminated', - 'EXISTS': 'instance_exists', - 'RUNNING': 'instance_running', - } - if state not in state_opts: - module.fail_json(msg="Cannot wait for state {0}, invalid state".format(state)) - waiter = module.client('ec2').get_waiter(state_opts[state]) - try: - waiter.wait( - InstanceIds=ids, - WaiterConfig={ - 'Delay': 15, - 'MaxAttempts': module.params.get('wait_timeout', 600) // 15, - } - ) - except botocore.exceptions.WaiterConfigError as e: - module.fail_json(msg="{0}. Error waiting for instances {1} to reach state {2}".format( - to_native(e), ', '.join(ids), state)) - except botocore.exceptions.WaiterError as e: - module.warn("Instances {0} took too long to reach state {1}. {2}".format( - ', '.join(ids), state, to_native(e))) - - -def diff_instance_and_params(instance, params, ec2=None, skip=None): - """boto3 instance obj, module params""" - if ec2 is None: - ec2 = module.client('ec2') - - if skip is None: - skip = [] - - changes_to_apply = [] - id_ = instance['InstanceId'] - - ParamMapper = namedtuple('ParamMapper', ['param_key', 'instance_key', 'attribute_name', 'add_value']) - - def value_wrapper(v): - return {'Value': v} - - param_mappings = [ - ParamMapper('ebs_optimized', 'EbsOptimized', 'ebsOptimized', value_wrapper), - ParamMapper('termination_protection', 'DisableApiTermination', 'disableApiTermination', value_wrapper), - # user data is an immutable property - # ParamMapper('user_data', 'UserData', 'userData', value_wrapper), - ] - - for mapping in param_mappings: - if params.get(mapping.param_key) is not None and mapping.instance_key not in skip: - value = AWSRetry.jittered_backoff()(ec2.describe_instance_attribute)(Attribute=mapping.attribute_name, InstanceId=id_) - if params.get(mapping.param_key) is not None and value[mapping.instance_key]['Value'] != params.get(mapping.param_key): - arguments = dict( - InstanceId=instance['InstanceId'], - # Attribute=mapping.attribute_name, - ) - arguments[mapping.instance_key] = mapping.add_value(params.get(mapping.param_key)) - changes_to_apply.append(arguments) - - if (params.get('network') or {}).get('source_dest_check') is not None: - # network.source_dest_check is nested, so needs to be treated separately - check = bool(params.get('network').get('source_dest_check')) - if instance['SourceDestCheck'] != check: - changes_to_apply.append(dict( - InstanceId=instance['InstanceId'], - SourceDestCheck={'Value': check}, - )) - - return changes_to_apply - - -def change_network_attachments(instance, params, ec2): - if (params.get('network') or {}).get('interfaces') is not None: - new_ids = [] - for inty in params.get('network').get('interfaces'): - if isinstance(inty, dict) and 'id' in inty: - new_ids.append(inty['id']) - elif isinstance(inty, string_types): - new_ids.append(inty) - # network.interfaces can create the need to attach new interfaces - old_ids = [inty['NetworkInterfaceId'] for inty in instance['NetworkInterfaces']] - to_attach = set(new_ids) - set(old_ids) - for eni_id in to_attach: - ec2.attach_network_interface( - DeviceIndex=new_ids.index(eni_id), - InstanceId=instance['InstanceId'], - NetworkInterfaceId=eni_id, - ) - return bool(len(to_attach)) - return False - - -def find_instances(ec2, ids=None, filters=None): - paginator = ec2.get_paginator('describe_instances') - if ids: - return list(paginator.paginate( - InstanceIds=ids, - ).search('Reservations[].Instances[]')) - elif filters is None: - module.fail_json(msg="No filters provided when they were required") - elif filters is not None: - for key in list(filters.keys()): - if not key.startswith("tag:"): - filters[key.replace("_", "-")] = filters.pop(key) - return list(paginator.paginate( - Filters=ansible_dict_to_boto3_filter_list(filters) - ).search('Reservations[].Instances[]')) - return [] - - -@AWSRetry.jittered_backoff() -def get_default_vpc(ec2): - vpcs = ec2.describe_vpcs(Filters=ansible_dict_to_boto3_filter_list({'isDefault': 'true'})) - if len(vpcs.get('Vpcs', [])): - return vpcs.get('Vpcs')[0] - return None - - -@AWSRetry.jittered_backoff() -def get_default_subnet(ec2, vpc, availability_zone=None): - subnets = ec2.describe_subnets( - Filters=ansible_dict_to_boto3_filter_list({ - 'vpc-id': vpc['VpcId'], - 'state': 'available', - 'default-for-az': 'true', - }) - ) - if len(subnets.get('Subnets', [])): - if availability_zone is not None: - subs_by_az = dict((subnet['AvailabilityZone'], subnet) for subnet in subnets.get('Subnets')) - if availability_zone in subs_by_az: - return subs_by_az[availability_zone] - - # to have a deterministic sorting order, we sort by AZ so we'll always pick the `a` subnet first - # there can only be one default-for-az subnet per AZ, so the AZ key is always unique in this list - by_az = sorted(subnets.get('Subnets'), key=lambda s: s['AvailabilityZone']) - return by_az[0] - return None - - -def ensure_instance_state(state, ec2=None): - if ec2 is None: - module.client('ec2') - if state in ('running', 'started'): - changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING') - - if failed: - module.fail_json( - msg="Unable to start instances: {0}".format(failure_reason), - reboot_success=list(changed), - reboot_failed=failed) - - module.exit_json( - msg='Instances started', - reboot_success=list(changed), - changed=bool(len(changed)), - reboot_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('restarted', 'rebooted'): - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='STOPPED') - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='RUNNING') - - if failed: - module.fail_json( - msg="Unable to restart instances: {0}".format(failure_reason), - reboot_success=list(changed), - reboot_failed=failed) - - module.exit_json( - msg='Instances restarted', - reboot_success=list(changed), - changed=bool(len(changed)), - reboot_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('stopped',): - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='STOPPED') - - if failed: - module.fail_json( - msg="Unable to stop instances: {0}".format(failure_reason), - stop_success=list(changed), - stop_failed=failed) - - module.exit_json( - msg='Instances stopped', - stop_success=list(changed), - changed=bool(len(changed)), - stop_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('absent', 'terminated'): - terminated, terminate_failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='TERMINATED') - - if terminate_failed: - module.fail_json( - msg="Unable to terminate instances: {0}".format(failure_reason), - terminate_success=list(terminated), - terminate_failed=terminate_failed) - module.exit_json( - msg='Instances terminated', - terminate_success=list(terminated), - changed=bool(len(terminated)), - terminate_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - - -@AWSRetry.jittered_backoff() -def change_instance_state(filters, desired_state, ec2=None): - """Takes STOPPED/RUNNING/TERMINATED""" - if ec2 is None: - ec2 = module.client('ec2') - - changed = set() - instances = find_instances(ec2, filters=filters) - to_change = set(i['InstanceId'] for i in instances if i['State']['Name'].upper() != desired_state) - unchanged = set() - failure_reason = "" - - for inst in instances: - try: - if desired_state == 'TERMINATED': - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - # TODO use a client-token to prevent double-sends of these start/stop/terminate commands - # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Run_Instance_Idempotency.html - resp = ec2.terminate_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['TerminatingInstances']] - if desired_state == 'STOPPED': - if inst['State']['Name'] in ('stopping', 'stopped'): - unchanged.add(inst['InstanceId']) - continue - - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - resp = ec2.stop_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['StoppingInstances']] - if desired_state == 'RUNNING': - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - resp = ec2.start_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['StartingInstances']] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - try: - failure_reason = to_native(e.message) - except AttributeError: - failure_reason = to_native(e) - - if changed: - await_instances(ids=list(changed) + list(unchanged), state=desired_state) - - change_failed = list(to_change - changed) - instances = find_instances(ec2, ids=list(i['InstanceId'] for i in instances)) - return changed, change_failed, instances, failure_reason - - -def pretty_instance(i): - instance = camel_dict_to_snake_dict(i, ignore_list=['Tags']) - instance['tags'] = boto3_tag_list_to_ansible_dict(i['Tags']) - return instance - - -def determine_iam_role(name_or_arn): - if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn): - return name_or_arn - iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) - try: - role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) - return role['InstanceProfile']['Arn'] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn)) - module.fail_json_aws(e, msg="An error occurred while searching for instance_role {0}. Please try supplying the full ARN.".format(name_or_arn)) - - -def handle_existing(existing_matches, changed, ec2, state): - if state in ('running', 'started') and [i for i in existing_matches if i['State']['Name'] != 'running']: - ins_changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING') - if failed: - module.fail_json(msg="Couldn't start instances: {0}. Failure reason: {1}".format(instances, failure_reason)) - module.exit_json( - changed=bool(len(ins_changed)) or changed, - instances=[pretty_instance(i) for i in instances], - instance_ids=[i['InstanceId'] for i in instances], - ) - changes = diff_instance_and_params(existing_matches[0], module.params) - for c in changes: - AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c) - changed |= bool(changes) - changed |= add_or_update_instance_profile(existing_matches[0], module.params.get('instance_role')) - changed |= change_network_attachments(existing_matches[0], module.params, ec2) - altered = find_instances(ec2, ids=[i['InstanceId'] for i in existing_matches]) - module.exit_json( - changed=bool(len(changes)) or changed, - instances=[pretty_instance(i) for i in altered], - instance_ids=[i['InstanceId'] for i in altered], - changes=changes, - ) - - -def ensure_present(existing_matches, changed, ec2, state): - if len(existing_matches): - try: - handle_existing(existing_matches, changed, ec2, state) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws( - e, msg="Failed to handle existing instances {0}".format(', '.join([i['InstanceId'] for i in existing_matches])), - # instances=[pretty_instance(i) for i in existing_matches], - # instance_ids=[i['InstanceId'] for i in existing_matches], - ) - try: - instance_spec = build_run_instance_spec(module.params) - # If check mode is enabled,suspend 'ensure function'. - if module.check_mode: - module.exit_json( - changed=True, - spec=instance_spec, - ) - instance_response = run_instances(ec2, **instance_spec) - instances = instance_response['Instances'] - instance_ids = [i['InstanceId'] for i in instances] - - for ins in instances: - changes = diff_instance_and_params(ins, module.params, skip=['UserData', 'EbsOptimized']) - for c in changes: - try: - AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, msg="Could not apply change {0} to new instance.".format(str(c))) - - if not module.params.get('wait'): - module.exit_json( - changed=True, - instance_ids=instance_ids, - spec=instance_spec, - ) - await_instances(instance_ids) - instances = ec2.get_paginator('describe_instances').paginate( - InstanceIds=instance_ids - ).search('Reservations[].Instances[]') - - module.exit_json( - changed=True, - instances=[pretty_instance(i) for i in instances], - instance_ids=instance_ids, - spec=instance_spec, - ) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Failed to create new EC2 instance") - - -@AWSRetry.jittered_backoff() -def run_instances(ec2, **instance_spec): - try: - return ec2.run_instances(**instance_spec) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidParameterValue' and "Invalid IAM Instance Profile ARN" in e.response['Error']['Message']: - # If the instance profile has just been created, it takes some time to be visible by ec2 - # So we wait 10 second and retry the run_instances - time.sleep(10) - return ec2.run_instances(**instance_spec) - else: - raise e - - -def main(): - global module - argument_spec = dict( - state=dict(default='present', choices=['present', 'started', 'running', 'stopped', 'restarted', 'rebooted', 'terminated', 'absent']), - wait=dict(default=True, type='bool'), - wait_timeout=dict(default=600, type='int'), - # count=dict(default=1, type='int'), - image=dict(type='dict'), - image_id=dict(type='str'), - instance_type=dict(default='t2.micro', type='str'), - user_data=dict(type='str'), - tower_callback=dict(type='dict'), - ebs_optimized=dict(type='bool'), - vpc_subnet_id=dict(type='str', aliases=['subnet_id']), - availability_zone=dict(type='str'), - security_groups=dict(default=[], type='list'), - security_group=dict(type='str'), - instance_role=dict(type='str'), - name=dict(type='str'), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=False), - filters=dict(type='dict', default=None), - launch_template=dict(type='dict'), - key_name=dict(type='str'), - cpu_credit_specification=dict(type='str', choices=['standard', 'unlimited']), - cpu_options=dict(type='dict', options=dict( - core_count=dict(type='int', required=True), - threads_per_core=dict(type='int', choices=[1, 2], required=True) - )), - tenancy=dict(type='str', choices=['dedicated', 'default']), - placement_group=dict(type='str'), - instance_initiated_shutdown_behavior=dict(type='str', choices=['stop', 'terminate']), - termination_protection=dict(type='bool'), - detailed_monitoring=dict(type='bool'), - instance_ids=dict(default=[], type='list'), - network=dict(default=None, type='dict'), - volumes=dict(default=None, type='list'), - ) - # running/present are synonyms - # as are terminated/absent - module = AnsibleAWSModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['security_groups', 'security_group'], - ['availability_zone', 'vpc_subnet_id'], - ['tower_callback', 'user_data'], - ['image_id', 'image'], - ], - supports_check_mode=True - ) - - if module.params.get('network'): - if module.params.get('network').get('interfaces'): - if module.params.get('security_group'): - module.fail_json(msg="Parameter network.interfaces can't be used with security_group") - if module.params.get('security_groups'): - module.fail_json(msg="Parameter network.interfaces can't be used with security_groups") - - state = module.params.get('state') - ec2 = module.client('ec2') - if module.params.get('filters') is None: - filters = { - # all states except shutting-down and terminated - 'instance-state-name': ['pending', 'running', 'stopping', 'stopped'] - } - if state == 'stopped': - # only need to change instances that aren't already stopped - filters['instance-state-name'] = ['stopping', 'pending', 'running'] - - if isinstance(module.params.get('instance_ids'), string_types): - filters['instance-id'] = [module.params.get('instance_ids')] - elif isinstance(module.params.get('instance_ids'), list) and len(module.params.get('instance_ids')): - filters['instance-id'] = module.params.get('instance_ids') - else: - if not module.params.get('vpc_subnet_id'): - if module.params.get('network'): - # grab AZ from one of the ENIs - ints = module.params.get('network').get('interfaces') - if ints: - filters['network-interface.network-interface-id'] = [] - for i in ints: - if isinstance(i, dict): - i = i['id'] - filters['network-interface.network-interface-id'].append(i) - else: - sub = get_default_subnet(ec2, get_default_vpc(ec2), availability_zone=module.params.get('availability_zone')) - filters['subnet-id'] = sub['SubnetId'] - else: - filters['subnet-id'] = [module.params.get('vpc_subnet_id')] - - if module.params.get('name'): - filters['tag:Name'] = [module.params.get('name')] - - if module.params.get('image_id'): - filters['image-id'] = [module.params.get('image_id')] - elif (module.params.get('image') or {}).get('id'): - filters['image-id'] = [module.params.get('image', {}).get('id')] - - module.params['filters'] = filters - - if module.params.get('cpu_options') and not module.botocore_at_least('1.10.16'): - module.fail_json(msg="cpu_options is only supported with botocore >= 1.10.16") - - existing_matches = find_instances(ec2, filters=module.params.get('filters')) - changed = False - - if state not in ('terminated', 'absent') and existing_matches: - for match in existing_matches: - warn_if_public_ip_assignment_changed(match) - warn_if_cpu_options_changed(match) - tags = module.params.get('tags') or {} - name = module.params.get('name') - if name: - tags['Name'] = name - changed |= manage_tags(match, tags, module.params.get('purge_tags', False), ec2) - - if state in ('present', 'running', 'started'): - ensure_present(existing_matches=existing_matches, changed=changed, ec2=ec2, state=state) - elif state in ('restarted', 'rebooted', 'stopped', 'absent', 'terminated'): - if existing_matches: - ensure_instance_state(state, ec2) - else: - module.exit_json( - msg='No matching instances found', - changed=False, - instances=[], - ) - else: - module.fail_json(msg="We don't handle the state {0}".format(state)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_instance_info.py b/lib/ansible/modules/cloud/amazon/ec2_instance_info.py deleted file mode 100644 index 7615b958d3..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_instance_info.py +++ /dev/null @@ -1,571 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ec2_instance_info -short_description: Gather information about ec2 instances in AWS -description: - - Gather information about ec2 instances in AWS - - This module was called C(ec2_instance_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -author: - - Michael Schuett (@michaeljs1990) - - Rob White (@wimnat) -requirements: [ "boto3", "botocore" ] -options: - instance_ids: - description: - - If you specify one or more instance IDs, only instances that have the specified IDs are returned. - required: false - version_added: 2.4 - type: list - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) for possible filters. Filter - names and values are case sensitive. - required: false - default: {} - type: dict - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all instances -- ec2_instance_info: - -# Gather information about all instances in AZ ap-southeast-2a -- ec2_instance_info: - filters: - availability-zone: ap-southeast-2a - -# Gather information about a particular instance using ID -- ec2_instance_info: - instance_ids: - - i-12345678 - -# Gather information about any instance with a tag key Name and value Example -- ec2_instance_info: - filters: - "tag:Name": Example - -# Gather information about any instance in states "shutting-down", "stopping", "stopped" -- ec2_instance_info: - filters: - instance-state-name: [ "shutting-down", "stopping", "stopped" ] - -''' - -RETURN = ''' -instances: - description: a list of ec2 instances - returned: always - type: complex - contains: - ami_launch_index: - description: The AMI launch index, which can be used to find this instance in the launch group. - returned: always - type: int - sample: 0 - architecture: - description: The architecture of the image - returned: always - type: str - sample: x86_64 - block_device_mappings: - description: Any block device mapping entries for the instance. - returned: always - type: complex - contains: - device_name: - description: The device name exposed to the instance (for example, /dev/sdh or xvdh). - returned: always - type: str - sample: /dev/sdh - ebs: - description: Parameters used to automatically set up EBS volumes when the instance is launched. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - delete_on_termination: - description: Indicates whether the volume is deleted on instance termination. - returned: always - type: bool - sample: true - status: - description: The attachment state. - returned: always - type: str - sample: attached - volume_id: - description: The ID of the EBS volume - returned: always - type: str - sample: vol-12345678 - cpu_options: - description: The CPU options set for the instance. - returned: always if botocore version >= 1.10.16 - type: complex - contains: - core_count: - description: The number of CPU cores for the instance. - returned: always - type: int - sample: 1 - threads_per_core: - description: The number of threads per CPU core. On supported instance, a value of 1 means Intel Hyper-Threading Technology is disabled. - returned: always - type: int - sample: 1 - client_token: - description: The idempotency token you provided when you launched the instance, if applicable. - returned: always - type: str - sample: mytoken - ebs_optimized: - description: Indicates whether the instance is optimized for EBS I/O. - returned: always - type: bool - sample: false - hypervisor: - description: The hypervisor type of the instance. - returned: always - type: str - sample: xen - iam_instance_profile: - description: The IAM instance profile associated with the instance, if applicable. - returned: always - type: complex - contains: - arn: - description: The Amazon Resource Name (ARN) of the instance profile. - returned: always - type: str - sample: "arn:aws:iam::000012345678:instance-profile/myprofile" - id: - description: The ID of the instance profile - returned: always - type: str - sample: JFJ397FDG400FG9FD1N - image_id: - description: The ID of the AMI used to launch the instance. - returned: always - type: str - sample: ami-0011223344 - instance_id: - description: The ID of the instance. - returned: always - type: str - sample: i-012345678 - instance_type: - description: The instance type size of the running instance. - returned: always - type: str - sample: t2.micro - key_name: - description: The name of the key pair, if this instance was launched with an associated key pair. - returned: always - type: str - sample: my-key - launch_time: - description: The time the instance was launched. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - monitoring: - description: The monitoring for the instance. - returned: always - type: complex - contains: - state: - description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. - returned: always - type: str - sample: disabled - network_interfaces: - description: One or more network interfaces for the instance. - returned: always - type: complex - contains: - association: - description: The association information for an Elastic IPv4 associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - attachment: - description: The network interface attachment. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - attachment_id: - description: The ID of the network interface attachment. - returned: always - type: str - sample: eni-attach-3aff3f - delete_on_termination: - description: Indicates whether the network interface is deleted when the instance is terminated. - returned: always - type: bool - sample: true - device_index: - description: The index of the device on the instance for the network interface attachment. - returned: always - type: int - sample: 0 - status: - description: The attachment state. - returned: always - type: str - sample: attached - description: - description: The description. - returned: always - type: str - sample: My interface - groups: - description: One or more security groups. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-abcdef12 - group_name: - description: The name of the security group. - returned: always - type: str - sample: mygroup - ipv6_addresses: - description: One or more IPv6 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - ipv6_address: - description: The IPv6 address. - returned: always - type: str - sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - mac_address: - description: The MAC address. - returned: always - type: str - sample: "00:11:22:33:44:55" - network_interface_id: - description: The ID of the network interface. - returned: always - type: str - sample: eni-01234567 - owner_id: - description: The AWS account ID of the owner of the network interface. - returned: always - type: str - sample: 01234567890 - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - private_ip_addresses: - description: The private IPv4 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - association: - description: The association information for an Elastic IP address (IPv4) associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - primary: - description: Indicates whether this IPv4 address is the primary private IP address of the network interface. - returned: always - type: bool - sample: true - private_ip_address: - description: The private IPv4 address of the network interface. - returned: always - type: str - sample: 10.0.0.1 - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - status: - description: The status of the network interface. - returned: always - type: str - sample: in-use - subnet_id: - description: The ID of the subnet for the network interface. - returned: always - type: str - sample: subnet-0123456 - vpc_id: - description: The ID of the VPC for the network interface. - returned: always - type: str - sample: vpc-0123456 - placement: - description: The location where the instance launched, if applicable. - returned: always - type: complex - contains: - availability_zone: - description: The Availability Zone of the instance. - returned: always - type: str - sample: ap-southeast-2a - group_name: - description: The name of the placement group the instance is in (for cluster compute instances). - returned: always - type: str - sample: "" - tenancy: - description: The tenancy of the instance (if the instance is running in a VPC). - returned: always - type: str - sample: default - private_dns_name: - description: The private DNS name. - returned: always - type: str - sample: ip-10-0-0-1.ap-southeast-2.compute.internal - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - product_codes: - description: One or more product codes. - returned: always - type: list - elements: dict - contains: - product_code_id: - description: The product code. - returned: always - type: str - sample: aw0evgkw8ef3n2498gndfgasdfsd5cce - product_code_type: - description: The type of product code. - returned: always - type: str - sample: marketplace - public_dns_name: - description: The public DNS name assigned to the instance. - returned: always - type: str - sample: - public_ip_address: - description: The public IPv4 address assigned to the instance - returned: always - type: str - sample: 52.0.0.1 - root_device_name: - description: The device name of the root device - returned: always - type: str - sample: /dev/sda1 - root_device_type: - description: The type of root device used by the AMI. - returned: always - type: str - sample: ebs - security_groups: - description: One or more security groups for the instance. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-0123456 - group_name: - description: The name of the security group. - returned: always - type: str - sample: my-security-group - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - state: - description: The current state of the instance. - returned: always - type: complex - contains: - code: - description: The low byte represents the state. - returned: always - type: int - sample: 16 - name: - description: The name of the state. - returned: always - type: str - sample: running - state_transition_reason: - description: The reason for the most recent state transition. - returned: always - type: str - sample: - subnet_id: - description: The ID of the subnet in which the instance is running. - returned: always - type: str - sample: subnet-00abcdef - tags: - description: Any tags assigned to the instance. - returned: always - type: dict - sample: - virtualization_type: - description: The type of virtualization of the AMI. - returned: always - type: str - sample: hvm - vpc_id: - description: The ID of the VPC the instance is in. - returned: always - type: dict - sample: vpc-0011223344 -''' - -import traceback - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, - boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -def list_ec2_instances(connection, module): - - instance_ids = module.params.get("instance_ids") - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - try: - reservations_paginator = connection.get_paginator('describe_instances') - reservations = reservations_paginator.paginate(InstanceIds=instance_ids, Filters=filters).build_full_result() - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - # Get instances from reservations - instances = [] - for reservation in reservations['Reservations']: - instances = instances + reservation['Instances'] - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_instances = [camel_dict_to_snake_dict(instance) for instance in instances] - - # Turn the boto3 result in to ansible friendly tag dictionary - for instance in snaked_instances: - instance['tags'] = boto3_tag_list_to_ansible_dict(instance.get('tags', []), 'key', 'value') - - module.exit_json(instances=snaked_instances) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - instance_ids=dict(default=[], type='list'), - filters=dict(default={}, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['instance_ids', 'filters'] - ], - supports_check_mode=True - ) - if module._name == 'ec2_instance_facts': - module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_ec2_instances(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_launch_template.py b/lib/ansible/modules/cloud/amazon/ec2_launch_template.py deleted file mode 100644 index d81f6fec25..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_launch_template.py +++ /dev/null @@ -1,702 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2018 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: ec2_launch_template -version_added: "2.8" -short_description: Manage EC2 launch templates -description: - - Create, modify, and delete EC2 Launch Templates, which can be used to - create individual instances or with Autoscaling Groups. - - The I(ec2_instance) and I(ec2_asg) modules can, instead of specifying all - parameters on those tasks, be passed a Launch Template which contains - settings like instance size, disk type, subnet, and more. -requirements: - - botocore - - boto3 >= 1.6.0 -extends_documentation_fragment: - - aws - - ec2 -author: - - Ryan Scott Brown (@ryansb) -options: - template_id: - description: - - The ID for the launch template, can be used for all cases except creating a new Launch Template. - aliases: [id] - type: str - template_name: - description: - - The template name. This must be unique in the region-account combination you are using. - aliases: [name] - type: str - default_version: - description: - - Which version should be the default when users spin up new instances based on this template? By default, the latest version will be made the default. - type: str - default: latest - state: - description: - - Whether the launch template should exist or not. - - Deleting specific versions of a launch template is not supported at this time. - choices: [present, absent] - default: present - type: str - block_device_mappings: - description: - - The block device mapping. Supplying both a snapshot ID and an encryption - value as arguments for block-device mapping results in an error. This is - because only blank volumes can be encrypted on start, and these are not - created from a snapshot. If a snapshot is the basis for the volume, it - contains data by definition and its encryption status cannot be changed - using this action. - type: list - elements: dict - suboptions: - device_name: - description: The device name (for example, /dev/sdh or xvdh). - type: str - no_device: - description: Suppresses the specified device included in the block device mapping of the AMI. - type: str - virtual_name: - description: > - The virtual device name (ephemeralN). Instance store volumes are - numbered starting from 0. An instance type with 2 available instance - store volumes can specify mappings for ephemeral0 and ephemeral1. The - number of available instance store volumes depends on the instance - type. After you connect to the instance, you must mount the volume. - type: str - ebs: - description: Parameters used to automatically set up EBS volumes when the instance is launched. - type: dict - suboptions: - delete_on_termination: - description: Indicates whether the EBS volume is deleted on instance termination. - type: bool - encrypted: - description: > - Indicates whether the EBS volume is encrypted. Encrypted volumes - can only be attached to instances that support Amazon EBS - encryption. If you are creating a volume from a snapshot, you - can't specify an encryption value. - type: bool - iops: - description: - - The number of I/O operations per second (IOPS) that the volume - supports. For io1, this represents the number of IOPS that are - provisioned for the volume. For gp2, this represents the baseline - performance of the volume and the rate at which the volume - accumulates I/O credits for bursting. For more information about - General Purpose SSD baseline performance, I/O credits, and - bursting, see Amazon EBS Volume Types in the Amazon Elastic - Compute Cloud User Guide. - - > - Condition: This parameter is required for requests to create io1 - volumes; it is not used in requests to create gp2, st1, sc1, or - standard volumes. - type: int - kms_key_id: - description: The ARN of the AWS Key Management Service (AWS KMS) CMK used for encryption. - type: str - snapshot_id: - description: The ID of the snapshot to create the volume from. - type: str - volume_size: - description: - - The size of the volume, in GiB. - - "Default: If you're creating the volume from a snapshot and don't specify a volume size, the default is the snapshot size." - type: int - volume_type: - description: The volume type - type: str - cpu_options: - description: - - Choose CPU settings for the EC2 instances that will be created with this template. - - For more information, see U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html) - type: dict - suboptions: - core_count: - description: The number of CPU cores for the instance. - type: int - threads_per_core: - description: > - The number of threads per CPU core. To disable Intel Hyper-Threading - Technology for the instance, specify a value of 1. Otherwise, specify - the default value of 2. - type: int - credit_specification: - description: The credit option for CPU usage of the instance. Valid for T2 or T3 instances only. - type: dict - suboptions: - cpu_credits: - description: > - The credit option for CPU usage of a T2 or T3 instance. Valid values - are C(standard) and C(unlimited). - type: str - disable_api_termination: - description: > - This helps protect instances from accidental termination. If set to true, - you can't terminate the instance using the Amazon EC2 console, CLI, or - API. To change this attribute to false after launch, use - I(ModifyInstanceAttribute). - type: bool - ebs_optimized: - description: > - Indicates whether the instance is optimized for Amazon EBS I/O. This - optimization provides dedicated throughput to Amazon EBS and an optimized - configuration stack to provide optimal Amazon EBS I/O performance. This - optimization isn't available with all instance types. Additional usage - charges apply when using an EBS-optimized instance. - type: bool - elastic_gpu_specifications: - type: list - elements: dict - description: Settings for Elastic GPU attachments. See U(https://aws.amazon.com/ec2/elastic-gpus/) for details. - suboptions: - type: - description: The type of Elastic GPU to attach - type: str - iam_instance_profile: - description: > - The name or ARN of an IAM instance profile. Requires permissions to - describe existing instance roles to confirm ARN is properly formed. - type: str - image_id: - description: > - The AMI ID to use for new instances launched with this template. This - value is region-dependent since AMIs are not global resources. - type: str - instance_initiated_shutdown_behavior: - description: > - Indicates whether an instance stops or terminates when you initiate - shutdown from the instance using the operating system shutdown command. - choices: [stop, terminate] - type: str - instance_market_options: - description: Options for alternative instance markets, currently only the spot market is supported. - type: dict - suboptions: - market_type: - description: The market type. This should always be 'spot'. - type: str - spot_options: - description: Spot-market specific settings. - type: dict - suboptions: - block_duration_minutes: - description: > - The required duration for the Spot Instances (also known as Spot - blocks), in minutes. This value must be a multiple of 60 (60, - 120, 180, 240, 300, or 360). - type: int - instance_interruption_behavior: - description: The behavior when a Spot Instance is interrupted. The default is C(terminate). - choices: [hibernate, stop, terminate] - type: str - max_price: - description: The highest hourly price you're willing to pay for this Spot Instance. - type: str - spot_instance_type: - description: The request type to send. - choices: [one-time, persistent] - type: str - instance_type: - description: > - The instance type, such as C(c5.2xlarge). For a full list of instance types, see - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html). - type: str - kernel_id: - description: > - The ID of the kernel. We recommend that you use PV-GRUB instead of - kernels and RAM disks. For more information, see - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) - type: str - key_name: - description: - - The name of the key pair. You can create a key pair using M(ec2_key). - - If you do not specify a key pair, you can't connect to the instance - unless you choose an AMI that is configured to allow users another way to - log in. - type: str - monitoring: - description: Settings for instance monitoring. - type: dict - suboptions: - enabled: - type: bool - description: Whether to turn on detailed monitoring for new instances. This will incur extra charges. - network_interfaces: - description: One or more network interfaces. - type: list - elements: dict - suboptions: - associate_public_ip_address: - description: Associates a public IPv4 address with eth0 for a new network interface. - type: bool - delete_on_termination: - description: Indicates whether the network interface is deleted when the instance is terminated. - type: bool - description: - description: A description for the network interface. - type: str - device_index: - description: The device index for the network interface attachment. - type: int - groups: - description: List of security group IDs to include on this instance. - type: list - elements: str - ipv6_address_count: - description: > - The number of IPv6 addresses to assign to a network interface. Amazon - EC2 automatically selects the IPv6 addresses from the subnet range. - You can't use this option if specifying the I(ipv6_addresses) option. - type: int - ipv6_addresses: - description: > - A list of one or more specific IPv6 addresses from the IPv6 CIDR - block range of your subnet. You can't use this option if you're - specifying the I(ipv6_address_count) option. - type: list - elements: str - network_interface_id: - description: The eni ID of a network interface to attach. - type: str - private_ip_address: - description: The primary private IPv4 address of the network interface. - type: str - subnet_id: - description: The ID of the subnet for the network interface. - type: str - placement: - description: The placement group settings for the instance. - type: dict - suboptions: - affinity: - description: The affinity setting for an instance on a Dedicated Host. - type: str - availability_zone: - description: The Availability Zone for the instance. - type: str - group_name: - description: The name of the placement group for the instance. - type: str - host_id: - description: The ID of the Dedicated Host for the instance. - type: str - tenancy: - description: > - The tenancy of the instance (if the instance is running in a VPC). An - instance with a tenancy of dedicated runs on single-tenant hardware. - type: str - ram_disk_id: - description: > - The ID of the RAM disk to launch the instance with. We recommend that you - use PV-GRUB instead of kernels and RAM disks. For more information, see - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) - type: str - security_group_ids: - description: A list of security group IDs (VPC or EC2-Classic) that the new instances will be added to. - type: list - elements: str - security_groups: - description: A list of security group names (VPC or EC2-Classic) that the new instances will be added to. - type: list - elements: str - tags: - type: dict - description: - - A set of key-value pairs to be applied to resources when this Launch Template is used. - - "Tag key constraints: Tag keys are case-sensitive and accept a maximum of 127 Unicode characters. May not begin with I(aws:)" - - "Tag value constraints: Tag values are case-sensitive and accept a maximum of 255 Unicode characters." - user_data: - description: > - The Base64-encoded user data to make available to the instance. For more information, see the Linux - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) and Windows - U(http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-instance-metadata.html#instancedata-add-user-data) - documentation on user-data. - type: str -''' - -EXAMPLES = ''' -- name: Create an ec2 launch template - ec2_launch_template: - name: "my_template" - image_id: "ami-04b762b4289fba92b" - key_name: my_ssh_key - instance_type: t2.micro - iam_instance_profile: myTestProfile - disable_api_termination: true - -- name: > - Create a new version of an existing ec2 launch template with a different instance type, - while leaving an older version as the default version - ec2_launch_template: - name: "my_template" - default_version: 1 - instance_type: c5.4xlarge - -- name: Delete an ec2 launch template - ec2_launch_template: - name: "my_template" - state: absent - -# This module does not yet allow deletion of specific versions of launch templates -''' - -RETURN = ''' -latest_version: - description: Latest available version of the launch template - returned: when state=present - type: int -default_version: - description: The version that will be used if only the template name is specified. Often this is the same as the latest version, but not always. - returned: when state=present - type: int -''' -import re -from uuid import uuid4 - -from ansible.module_utils._text import to_text -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict, snake_dict_to_camel_dict -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, AWSRetry, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list - -try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError -except ImportError: - pass # caught by AnsibleAWSModule - - -def determine_iam_role(module, name_or_arn): - if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn): - return name_or_arn - iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) - try: - role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) - return {'arn': role['InstanceProfile']['Arn']} - except is_boto3_error_code('NoSuchEntity') as e: - module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn)) - except (BotoCoreError, ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="An error occurred while searching for instance_role {0}. Please try supplying the full ARN.".format(name_or_arn)) - - -def existing_templates(module): - ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - matches = None - try: - if module.params.get('template_id'): - matches = ec2.describe_launch_templates(LaunchTemplateIds=[module.params.get('template_id')]) - elif module.params.get('template_name'): - matches = ec2.describe_launch_templates(LaunchTemplateNames=[module.params.get('template_name')]) - except is_boto3_error_code('InvalidLaunchTemplateName.NotFoundException') as e: - # no named template was found, return nothing/empty versions - return None, [] - except is_boto3_error_code('InvalidLaunchTemplateId.Malformed') as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg='Launch template with ID {0} is not a valid ID. It should start with `lt-....`'.format( - module.params.get('launch_template_id'))) - except is_boto3_error_code('InvalidLaunchTemplateId.NotFoundException') as e: # pylint: disable=duplicate-except - module.fail_json_aws( - e, msg='Launch template with ID {0} could not be found, please supply a name ' - 'instead so that a new template can be created'.format(module.params.get('launch_template_id'))) - except (ClientError, BotoCoreError, WaiterError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg='Could not check existing launch templates. This may be an IAM permission problem.') - else: - template = matches['LaunchTemplates'][0] - template_id, template_version, template_default = template['LaunchTemplateId'], template['LatestVersionNumber'], template['DefaultVersionNumber'] - try: - return template, ec2.describe_launch_template_versions(LaunchTemplateId=template_id)['LaunchTemplateVersions'] - except (ClientError, BotoCoreError, WaiterError) as e: - module.fail_json_aws(e, msg='Could not find launch template versions for {0} (ID: {1}).'.format(template['LaunchTemplateName'], template_id)) - - -def params_to_launch_data(module, template_params): - if template_params.get('tags'): - template_params['tag_specifications'] = [ - { - 'resource_type': r_type, - 'tags': [ - {'Key': k, 'Value': v} for k, v - in template_params['tags'].items() - ] - } - for r_type in ('instance', 'volume') - ] - del template_params['tags'] - if module.params.get('iam_instance_profile'): - template_params['iam_instance_profile'] = determine_iam_role(module, module.params['iam_instance_profile']) - params = snake_dict_to_camel_dict( - dict((k, v) for k, v in template_params.items() if v is not None), - capitalize_first=True, - ) - return params - - -def delete_template(module): - ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - template, template_versions = existing_templates(module) - deleted_versions = [] - if template or template_versions: - non_default_versions = [to_text(t['VersionNumber']) for t in template_versions if not t['DefaultVersion']] - if non_default_versions: - try: - v_resp = ec2.delete_launch_template_versions( - LaunchTemplateId=template['LaunchTemplateId'], - Versions=non_default_versions, - ) - if v_resp['UnsuccessfullyDeletedLaunchTemplateVersions']: - module.warn('Failed to delete template versions {0} on launch template {1}'.format( - v_resp['UnsuccessfullyDeletedLaunchTemplateVersions'], - template['LaunchTemplateId'], - )) - deleted_versions = [camel_dict_to_snake_dict(v) for v in v_resp['SuccessfullyDeletedLaunchTemplateVersions']] - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not delete existing versions of the launch template {0}".format(template['LaunchTemplateId'])) - try: - resp = ec2.delete_launch_template( - LaunchTemplateId=template['LaunchTemplateId'], - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not delete launch template {0}".format(template['LaunchTemplateId'])) - return { - 'deleted_versions': deleted_versions, - 'deleted_template': camel_dict_to_snake_dict(resp['LaunchTemplate']), - 'changed': True, - } - else: - return {'changed': False} - - -def create_or_update(module, template_options): - ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidLaunchTemplateId.NotFound'])) - template, template_versions = existing_templates(module) - out = {} - lt_data = params_to_launch_data(module, dict((k, v) for k, v in module.params.items() if k in template_options)) - if not (template or template_versions): - # create a full new one - try: - resp = ec2.create_launch_template( - LaunchTemplateName=module.params['template_name'], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - aws_retry=True, - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create launch template") - template, template_versions = existing_templates(module) - out['changed'] = True - elif template and template_versions: - most_recent = sorted(template_versions, key=lambda x: x['VersionNumber'])[-1] - if lt_data == most_recent['LaunchTemplateData']: - out['changed'] = False - return out - try: - resp = ec2.create_launch_template_version( - LaunchTemplateId=template['LaunchTemplateId'], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - aws_retry=True, - ) - if module.params.get('default_version') in (None, ''): - # no need to do anything, leave the existing version as default - pass - elif module.params.get('default_version') == 'latest': - set_default = ec2.modify_launch_template( - LaunchTemplateId=template['LaunchTemplateId'], - DefaultVersion=to_text(resp['LaunchTemplateVersion']['VersionNumber']), - ClientToken=uuid4().hex, - aws_retry=True, - ) - else: - try: - int(module.params.get('default_version')) - except ValueError: - module.fail_json(msg='default_version param was not a valid integer, got "{0}"'.format(module.params.get('default_version'))) - set_default = ec2.modify_launch_template( - LaunchTemplateId=template['LaunchTemplateId'], - DefaultVersion=to_text(int(module.params.get('default_version'))), - ClientToken=uuid4().hex, - aws_retry=True, - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create subsequent launch template version") - template, template_versions = existing_templates(module) - out['changed'] = True - return out - - -def format_module_output(module): - output = {} - template, template_versions = existing_templates(module) - template = camel_dict_to_snake_dict(template) - template_versions = [camel_dict_to_snake_dict(v) for v in template_versions] - for v in template_versions: - for ts in (v['launch_template_data'].get('tag_specifications') or []): - ts['tags'] = boto3_tag_list_to_ansible_dict(ts.pop('tags')) - output.update(dict(template=template, versions=template_versions)) - output['default_template'] = [ - v for v in template_versions - if v.get('default_version') - ][0] - output['latest_template'] = [ - v for v in template_versions - if ( - v.get('version_number') and - int(v['version_number']) == int(template['latest_version_number']) - ) - ][0] - if "version_number" in output['default_template']: - output['default_version'] = output['default_template']['version_number'] - if "version_number" in output['latest_template']: - output['latest_version'] = output['latest_template']['version_number'] - return output - - -def main(): - template_options = dict( - block_device_mappings=dict( - type='list', - options=dict( - device_name=dict(), - ebs=dict( - type='dict', - options=dict( - delete_on_termination=dict(type='bool'), - encrypted=dict(type='bool'), - iops=dict(type='int'), - kms_key_id=dict(), - snapshot_id=dict(), - volume_size=dict(type='int'), - volume_type=dict(), - ), - ), - no_device=dict(), - virtual_name=dict(), - ), - ), - cpu_options=dict( - type='dict', - options=dict( - core_count=dict(type='int'), - threads_per_core=dict(type='int'), - ), - ), - credit_specification=dict( - dict(type='dict'), - options=dict( - cpu_credits=dict(), - ), - ), - disable_api_termination=dict(type='bool'), - ebs_optimized=dict(type='bool'), - elastic_gpu_specifications=dict( - options=dict(type=dict()), - type='list', - ), - iam_instance_profile=dict(), - image_id=dict(), - instance_initiated_shutdown_behavior=dict(choices=['stop', 'terminate']), - instance_market_options=dict( - type='dict', - options=dict( - market_type=dict(), - spot_options=dict( - type='dict', - options=dict( - block_duration_minutes=dict(type='int'), - instance_interruption_behavior=dict(choices=['hibernate', 'stop', 'terminate']), - max_price=dict(), - spot_instance_type=dict(choices=['one-time', 'persistent']), - ), - ), - ), - ), - instance_type=dict(), - kernel_id=dict(), - key_name=dict(), - monitoring=dict( - type='dict', - options=dict( - enabled=dict(type='bool') - ), - ), - network_interfaces=dict( - type='list', - options=dict( - associate_public_ip_address=dict(type='bool'), - delete_on_termination=dict(type='bool'), - description=dict(), - device_index=dict(type='int'), - groups=dict(type='list'), - ipv6_address_count=dict(type='int'), - ipv6_addresses=dict(type='list'), - network_interface_id=dict(), - private_ip_address=dict(), - subnet_id=dict(), - ), - ), - placement=dict( - options=dict( - affinity=dict(), - availability_zone=dict(), - group_name=dict(), - host_id=dict(), - tenancy=dict(), - ), - type='dict', - ), - ram_disk_id=dict(), - security_group_ids=dict(type='list'), - security_groups=dict(type='list'), - tags=dict(type='dict'), - user_data=dict(), - ) - - arg_spec = dict( - state=dict(choices=['present', 'absent'], default='present'), - template_name=dict(aliases=['name']), - template_id=dict(aliases=['id']), - default_version=dict(default='latest'), - ) - - arg_spec.update(template_options) - - module = AnsibleAWSModule( - argument_spec=arg_spec, - required_one_of=[ - ('template_name', 'template_id') - ], - supports_check_mode=True - ) - - if not module.boto3_at_least('1.6.0'): - module.fail_json(msg="ec2_launch_template requires boto3 >= 1.6.0") - - for interface in (module.params.get('network_interfaces') or []): - if interface.get('ipv6_addresses'): - interface['ipv6_addresses'] = [{'ipv6_address': x} for x in interface['ipv6_addresses']] - - if module.params.get('state') == 'present': - out = create_or_update(module, template_options) - out.update(format_module_output(module)) - elif module.params.get('state') == 'absent': - out = delete_template(module) - else: - module.fail_json(msg='Unsupported value "{0}" for `state` parameter'.format(module.params.get('state'))) - - module.exit_json(**out) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_lc.py b/lib/ansible/modules/cloud/amazon/ec2_lc.py deleted file mode 100644 index 82d4364aee..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_lc.py +++ /dev/null @@ -1,714 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_lc - -short_description: Create or delete AWS Autoscaling Launch Configurations - -description: - - Can create or delete AWS Autoscaling Configurations. - - Works with the ec2_asg module to manage Autoscaling Groups. - -notes: - - Amazon ASG Autoscaling Launch Configurations are immutable once created, so modifying the configuration after it is changed will not modify the - launch configuration on AWS. You must create a new config and assign it to the ASG instead. - - encrypted volumes are supported on versions >= 2.4 - -version_added: "1.6" - -author: - - "Gareth Rushgrove (@garethr)" - - "Willem van Ketwich (@wilvk)" - -options: - state: - description: - - Register or deregister the instance. - default: present - choices: ['present', 'absent'] - type: str - name: - description: - - Unique name for configuration. - required: true - type: str - instance_type: - description: - - Instance type to use for the instance. - - Required when creating a new Launch Configuration. - type: str - image_id: - description: - - The AMI unique identifier to be used for the group. - type: str - key_name: - description: - - The SSH key name to be used for access to managed instances. - type: str - security_groups: - description: - - A list of security groups to apply to the instances. Since version 2.4 you can specify either security group names or IDs or a mix. Previous - to 2.4, for VPC instances, specify security group IDs and for EC2-Classic, specify either security group names or IDs. - type: list - elements: str - volumes: - description: - - A list dictionaries defining the volumes to create. - - For any volume, a volume size less than 1 will be interpreted as a request not to create the volume. - type: list - elements: dict - suboptions: - device_name: - type: str - description: - - The name for the volume (For example C(/dev/sda)). - required: true - no_device: - type: bool - description: - - When I(no_device=true) the device will not be created. - snapshot: - type: str - description: - - The ID of an EBS snapshot to copy when creating the volume. - - Mutually exclusive with the I(ephemeral) parameter. - ephemeral: - type: str - description: - - Whether the volume should be ephemeral. - - Data on ephemeral volumes is lost when the instance is stopped. - - Mutually exclusive with the I(snapshot) parameter. - volume_size: - type: int - description: - - The size of the volume (in GiB). - - Required unless one of I(ephemeral), I(snapshot) or I(no_device) is set. - volume_type: - type: str - description: - - The type of volume to create. - - See - U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) for more information on the available volume types. - delete_on_termination: - type: bool - default: false - description: - - Whether the volume should be automatically deleted when the instance - is terminated. - iops: - type: int - description: - - The number of IOPS per second to provision for the volume. - - Required when I(volume_type=io1). - encrypted: - type: bool - default: false - description: - - Whether the volume should be encrypted using the 'aws/ebs' KMS CMK. - user_data: - description: - - Opaque blob of data which is made available to the ec2 instance. Mutually exclusive with I(user_data_path). - type: str - user_data_path: - description: - - Path to the file that contains userdata for the ec2 instances. Mutually exclusive with I(user_data). - version_added: "2.3" - type: path - kernel_id: - description: - - Kernel id for the EC2 instance. - type: str - spot_price: - description: - - The spot price you are bidding. Only applies for an autoscaling group with spot instances. - type: float - instance_monitoring: - description: - - Specifies whether instances are launched with detailed monitoring. - type: bool - default: false - assign_public_ip: - description: - - Used for Auto Scaling groups that launch instances into an Amazon Virtual Private Cloud. Specifies whether to assign a public IP address - to each instance launched in a Amazon VPC. - version_added: "1.8" - type: bool - ramdisk_id: - description: - - A RAM disk id for the instances. - version_added: "1.8" - type: str - instance_profile_name: - description: - - The name or the Amazon Resource Name (ARN) of the instance profile associated with the IAM role for the instances. - version_added: "1.8" - type: str - ebs_optimized: - description: - - Specifies whether the instance is optimized for EBS I/O (true) or not (false). - default: false - version_added: "1.8" - type: bool - classic_link_vpc_id: - description: - - Id of ClassicLink enabled VPC - version_added: "2.0" - type: str - classic_link_vpc_security_groups: - description: - - A list of security group IDs with which to associate the ClassicLink VPC instances. - version_added: "2.0" - type: list - elements: str - vpc_id: - description: - - VPC ID, used when resolving security group names to IDs. - version_added: "2.4" - type: str - instance_id: - description: - - The Id of a running instance to use as a basis for a launch configuration. Can be used in place of I(image_id) and I(instance_type). - version_added: "2.4" - type: str - placement_tenancy: - description: - - Determines whether the instance runs on single-tenant hardware or not. - - When not set AWS will default to C(default). - version_added: "2.4" - type: str - choices: ['default', 'dedicated'] - associate_public_ip_address: - description: - - The I(associate_public_ip_address) option does nothing and will be removed in Ansible 2.14. - type: bool - -extends_documentation_fragment: - - aws - - ec2 - -requirements: - - boto3 >= 1.4.4 - -''' - -EXAMPLES = ''' - -# create a launch configuration using an AMI image and instance type as a basis - -- name: note that encrypted volumes are only supported in >= Ansible 2.4 - ec2_lc: - name: special - image_id: ami-XXX - key_name: default - security_groups: ['group', 'group2' ] - instance_type: t1.micro - volumes: - - device_name: /dev/sda1 - volume_size: 100 - volume_type: io1 - iops: 3000 - delete_on_termination: true - encrypted: true - - device_name: /dev/sdb - ephemeral: ephemeral0 - -# create a launch configuration using a running instance id as a basis - -- ec2_lc: - name: special - instance_id: i-00a48b207ec59e948 - key_name: default - security_groups: ['launch-wizard-2' ] - volumes: - - device_name: /dev/sda1 - volume_size: 120 - volume_type: io1 - iops: 3000 - delete_on_termination: true - -# create a launch configuration to omit the /dev/sdf EBS device that is included in the AMI image - -- ec2_lc: - name: special - image_id: ami-XXX - key_name: default - security_groups: ['group', 'group2' ] - instance_type: t1.micro - volumes: - - device_name: /dev/sdf - no_device: true - -- name: Use EBS snapshot ID for volume - block: - - name: Set Volume Facts - set_fact: - volumes: - - device_name: /dev/sda1 - volume_size: 20 - ebs: - snapshot: snap-XXXX - volume_type: gp2 - delete_on_termination: true - encrypted: no - - - name: Create launch configuration - ec2_lc: - name: lc1 - image_id: ami-xxxx - assign_public_ip: yes - instance_type: t2.medium - key_name: my-key - security_groups: "['sg-xxxx']" - volumes: "{{ volumes }}" - register: lc_info -''' - -RETURN = ''' -arn: - description: The Amazon Resource Name of the launch configuration. - returned: when I(state=present) - type: str - sample: arn:aws:autoscaling:us-east-1:148830907657:launchConfiguration:888d9b58-d93a-40c4-90cf-759197a2621a:launchConfigurationName/launch_config_name -changed: - description: Whether the state of the launch configuration has changed. - returned: always - type: bool - sample: false -created_time: - description: The creation date and time for the launch configuration. - returned: when I(state=present) - type: str - sample: '2017-11-03 23:46:44.841000' -image_id: - description: The ID of the Amazon Machine Image used by the launch configuration. - returned: when I(state=present) - type: str - sample: ami-9be6f38c -instance_type: - description: The instance type for the instances. - returned: when I(state=present) - type: str - sample: t1.micro -name: - description: The name of the launch configuration. - returned: when I(state=present) - type: str - sample: launch_config_name -result: - description: The specification details for the launch configuration. - returned: when I(state=present) - type: complex - contains: - PlacementTenancy: - description: The tenancy of the instances, either default or dedicated. - returned: when I(state=present) - type: str - sample: default - associate_public_ip_address: - description: (EC2-VPC) Indicates whether to assign a public IP address to each instance. - returned: when I(state=present) - type: bool - sample: false - block_device_mappings: - description: A block device mapping, which specifies the block devices. - returned: when I(state=present) - type: complex - contains: - device_name: - description: The device name exposed to the EC2 instance (for example, /dev/sdh or xvdh). - returned: when I(state=present) - type: str - sample: /dev/sda1 - ebs: - description: The information about the Amazon EBS volume. - returned: when I(state=present) - type: complex - contains: - snapshot_id: - description: The ID of the snapshot. - returned: when I(state=present) - type: str - volume_size: - description: The volume size, in GiB. - returned: when I(state=present) - type: str - sample: '100' - virtual_name: - description: The name of the virtual device (for example, ephemeral0). - returned: when I(state=present) - type: str - sample: ephemeral0 - classic_link_vpc_id: - description: The ID of a ClassicLink-enabled VPC to link your EC2-Classic instances to. - returned: when I(state=present) - type: str - classic_link_vpc_security_groups: - description: The IDs of one or more security groups for the VPC specified in ClassicLinkVPCId. - returned: when I(state=present) - type: list - sample: [] - created_time: - description: The creation date and time for the launch configuration. - returned: when I(state=present) - type: str - sample: '2017-11-03 23:46:44.841000' - delete_on_termination: - description: Indicates whether the volume is deleted on instance termination. - returned: when I(state=present) - type: bool - sample: true - ebs_optimized: - description: Indicates whether the instance is optimized for EBS I/O (true) or not (false). - returned: when I(state=present) - type: bool - sample: false - image_id: - description: The ID of the Amazon Machine Image used by the launch configuration. - returned: when I(state=present) - type: str - sample: ami-9be6f38c - instance_monitoring: - description: Indicates whether instances in this group are launched with detailed (true) or basic (false) monitoring. - returned: when I(state=present) - type: bool - sample: true - instance_profile_name: - description: The name or Amazon Resource Name (ARN) of the instance profile associated with the IAM role for the instance. - returned: when I(state=present) - type: str - sample: null - instance_type: - description: The instance type for the instances. - returned: when I(state=present) - type: str - sample: t1.micro - iops: - description: The number of I/O operations per second (IOPS) to provision for the volume. - returned: when I(state=present) - type: int - kernel_id: - description: The ID of the kernel associated with the AMI. - returned: when I(state=present) - type: str - sample: '' - key_name: - description: The name of the key pair. - returned: when I(state=present) - type: str - sample: testkey - launch_configuration_arn: - description: The Amazon Resource Name (ARN) of the launch configuration. - returned: when I(state=present) - type: str - sample: arn:aws:autoscaling:us-east-1:148830907657:launchConfiguration:888d9b58-d93a-40c4-90cf-759197a2621a:launchConfigurationName/launch_config_name - member: - description: "" - returned: when I(state=present) - type: str - sample: "\n " - name: - description: The name of the launch configuration. - returned: when I(state=present) - type: str - sample: launch_config_name - ramdisk_id: - description: The ID of the RAM disk associated with the AMI. - returned: when I(state=present) - type: str - sample: '' - security_groups: - description: The security groups to associate with the instances. - returned: when I(state=present) - type: list - sample: - - sg-5e27db2f - spot_price: - description: The price to bid when launching Spot Instances. - returned: when I(state=present) - type: float - use_block_device_types: - description: Indicates whether to suppress a device mapping. - returned: when I(state=present) - type: bool - sample: false - user_data: - description: The user data available to the instances. - returned: when I(state=present) - type: str - sample: '' - volume_type: - description: The volume type (one of standard, io1, gp2). - returned: when I(state=present) - type: str - sample: io1 -security_groups: - description: The security groups to associate with the instances. - returned: when I(state=present) - type: list - sample: - - sg-5e27db2f - -''' - - -import traceback -from ansible.module_utils.ec2 import (get_aws_connection_info, ec2_argument_spec, ec2_connect, camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names, - boto3_conn, snake_dict_to_camel_dict, HAS_BOTO3) -from ansible.module_utils._text import to_text -from ansible.module_utils.basic import AnsibleModule - -try: - import botocore -except ImportError: - pass - - -def create_block_device_meta(module, volume): - if 'snapshot' not in volume and 'ephemeral' not in volume and 'no_device' not in volume: - if 'volume_size' not in volume: - module.fail_json(msg='Size must be specified when creating a new volume or modifying the root volume') - if 'snapshot' in volume: - if volume.get('volume_type') == 'io1' and 'iops' not in volume: - module.fail_json(msg='io1 volumes must have an iops value set') - if 'ephemeral' in volume: - if 'snapshot' in volume: - module.fail_json(msg='Cannot set both ephemeral and snapshot') - - return_object = {} - - if 'ephemeral' in volume: - return_object['VirtualName'] = volume.get('ephemeral') - - if 'device_name' in volume: - return_object['DeviceName'] = volume.get('device_name') - - if 'no_device' in volume: - return_object['NoDevice'] = volume.get('no_device') - - if any(key in volume for key in ['snapshot', 'volume_size', 'volume_type', 'delete_on_termination', 'ips', 'encrypted']): - return_object['Ebs'] = {} - - if 'snapshot' in volume: - return_object['Ebs']['SnapshotId'] = volume.get('snapshot') - - if 'volume_size' in volume: - return_object['Ebs']['VolumeSize'] = int(volume.get('volume_size', 0)) - - if 'volume_type' in volume: - return_object['Ebs']['VolumeType'] = volume.get('volume_type') - - if 'delete_on_termination' in volume: - return_object['Ebs']['DeleteOnTermination'] = volume.get('delete_on_termination', False) - - if 'iops' in volume: - return_object['Ebs']['Iops'] = volume.get('iops') - - if 'encrypted' in volume: - return_object['Ebs']['Encrypted'] = volume.get('encrypted') - - return return_object - - -def create_launch_config(connection, module): - name = module.params.get('name') - vpc_id = module.params.get('vpc_id') - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - ec2_connection = boto3_conn(module, 'client', 'ec2', region, ec2_url, **aws_connect_kwargs) - security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), ec2_connection, vpc_id=vpc_id, boto3=True) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to get Security Group IDs", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except ValueError as e: - module.fail_json(msg="Failed to get Security Group IDs", exception=traceback.format_exc()) - user_data = module.params.get('user_data') - user_data_path = module.params.get('user_data_path') - volumes = module.params['volumes'] - instance_monitoring = module.params.get('instance_monitoring') - assign_public_ip = module.params.get('assign_public_ip') - instance_profile_name = module.params.get('instance_profile_name') - ebs_optimized = module.params.get('ebs_optimized') - classic_link_vpc_id = module.params.get('classic_link_vpc_id') - classic_link_vpc_security_groups = module.params.get('classic_link_vpc_security_groups') - - block_device_mapping = [] - - convert_list = ['image_id', 'instance_type', 'instance_type', 'instance_id', 'placement_tenancy', 'key_name', 'kernel_id', 'ramdisk_id', 'spot_price'] - - launch_config = (snake_dict_to_camel_dict(dict((k.capitalize(), str(v)) for k, v in module.params.items() if v is not None and k in convert_list))) - - if user_data_path: - try: - with open(user_data_path, 'r') as user_data_file: - user_data = user_data_file.read() - except IOError as e: - module.fail_json(msg="Failed to open file for reading", exception=traceback.format_exc()) - - if volumes: - for volume in volumes: - if 'device_name' not in volume: - module.fail_json(msg='Device name must be set for volume') - # Minimum volume size is 1GiB. We'll use volume size explicitly set to 0 to be a signal not to create this volume - if 'volume_size' not in volume or int(volume['volume_size']) > 0: - block_device_mapping.append(create_block_device_meta(module, volume)) - - try: - launch_configs = connection.describe_launch_configurations(LaunchConfigurationNames=[name]).get('LaunchConfigurations') - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to describe launch configuration by name", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - changed = False - result = {} - - launch_config['LaunchConfigurationName'] = name - - if security_groups is not None: - launch_config['SecurityGroups'] = security_groups - - if classic_link_vpc_id is not None: - launch_config['ClassicLinkVPCId'] = classic_link_vpc_id - - if instance_monitoring is not None: - launch_config['InstanceMonitoring'] = {'Enabled': instance_monitoring} - - if classic_link_vpc_security_groups is not None: - launch_config['ClassicLinkVPCSecurityGroups'] = classic_link_vpc_security_groups - - if block_device_mapping: - launch_config['BlockDeviceMappings'] = block_device_mapping - - if instance_profile_name is not None: - launch_config['IamInstanceProfile'] = instance_profile_name - - if assign_public_ip is not None: - launch_config['AssociatePublicIpAddress'] = assign_public_ip - - if user_data is not None: - launch_config['UserData'] = user_data - - if ebs_optimized is not None: - launch_config['EbsOptimized'] = ebs_optimized - - if len(launch_configs) == 0: - try: - connection.create_launch_configuration(**launch_config) - launch_configs = connection.describe_launch_configurations(LaunchConfigurationNames=[name]).get('LaunchConfigurations') - changed = True - if launch_configs: - launch_config = launch_configs[0] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to create launch configuration", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - result = (dict((k, v) for k, v in launch_config.items() - if k not in ['Connection', 'CreatedTime', 'InstanceMonitoring', 'BlockDeviceMappings'])) - - result['CreatedTime'] = to_text(launch_config.get('CreatedTime')) - - try: - result['InstanceMonitoring'] = module.boolean(launch_config.get('InstanceMonitoring').get('Enabled')) - except AttributeError: - result['InstanceMonitoring'] = False - - result['BlockDeviceMappings'] = [] - - for block_device_mapping in launch_config.get('BlockDeviceMappings', []): - result['BlockDeviceMappings'].append(dict(device_name=block_device_mapping.get('DeviceName'), virtual_name=block_device_mapping.get('VirtualName'))) - if block_device_mapping.get('Ebs') is not None: - result['BlockDeviceMappings'][-1]['ebs'] = dict( - snapshot_id=block_device_mapping.get('Ebs').get('SnapshotId'), volume_size=block_device_mapping.get('Ebs').get('VolumeSize')) - - if user_data_path: - result['UserData'] = "hidden" # Otherwise, we dump binary to the user's terminal - - return_object = { - 'Name': result.get('LaunchConfigurationName'), - 'CreatedTime': result.get('CreatedTime'), - 'ImageId': result.get('ImageId'), - 'Arn': result.get('LaunchConfigurationARN'), - 'SecurityGroups': result.get('SecurityGroups'), - 'InstanceType': result.get('InstanceType'), - 'Result': result - } - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(return_object)) - - -def delete_launch_config(connection, module): - try: - name = module.params.get('name') - launch_configs = connection.describe_launch_configurations(LaunchConfigurationNames=[name]).get('LaunchConfigurations') - if launch_configs: - connection.delete_launch_configuration(LaunchConfigurationName=launch_configs[0].get('LaunchConfigurationName')) - module.exit_json(changed=True) - else: - module.exit_json(changed=False) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Failed to delete launch configuration", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True), - image_id=dict(), - instance_id=dict(), - key_name=dict(), - security_groups=dict(default=[], type='list'), - user_data=dict(), - user_data_path=dict(type='path'), - kernel_id=dict(), - volumes=dict(type='list'), - instance_type=dict(), - state=dict(default='present', choices=['present', 'absent']), - spot_price=dict(type='float'), - ramdisk_id=dict(), - instance_profile_name=dict(), - ebs_optimized=dict(default=False, type='bool'), - associate_public_ip_address=dict(type='bool', removed_in_version='2.14'), - instance_monitoring=dict(default=False, type='bool'), - assign_public_ip=dict(type='bool'), - classic_link_vpc_security_groups=dict(type='list'), - classic_link_vpc_id=dict(), - vpc_id=dict(), - placement_tenancy=dict(choices=['default', 'dedicated']) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['user_data', 'user_data_path']] - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='autoscaling', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoRegionError: - module.fail_json(msg=("region must be specified as a parameter in AWS_DEFAULT_REGION environment variable or in boto configuration file")) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="unable to establish connection - " + str(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - state = module.params.get('state') - - if state == 'present': - create_launch_config(connection, module) - elif state == 'absent': - delete_launch_config(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_lc_find.py b/lib/ansible/modules/cloud/amazon/ec2_lc_find.py deleted file mode 100644 index a972e7f052..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_lc_find.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/python -# encoding: utf-8 - -# (c) 2015, Jose Armesto <jose@armesto.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_lc_find -short_description: Find AWS Autoscaling Launch Configurations -description: - - Returns list of matching Launch Configurations for a given name, along with other useful information. - - Results can be sorted and sliced. - - It depends on boto. - - Based on the work by Tom Bamford U(https://github.com/tombamford) - -version_added: "2.2" -author: "Jose Armesto (@fiunchinho)" -options: - name_regex: - description: - - A Launch Configuration to match. - - It'll be compiled as regex. - required: True - type: str - sort_order: - description: - - Order in which to sort results. - choices: ['ascending', 'descending'] - default: 'ascending' - type: str - limit: - description: - - How many results to show. - - Corresponds to Python slice notation like list[:limit]. - type: int -requirements: - - "python >= 2.6" - - boto3 -extends_documentation_fragment: - - ec2 - - aws -""" - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Search for the Launch Configurations that start with "app" -- ec2_lc_find: - name_regex: app.* - sort_order: descending - limit: 2 -''' - -RETURN = ''' -image_id: - description: AMI id - returned: when Launch Configuration was found - type: str - sample: "ami-0d75df7e" -user_data: - description: User data used to start instance - returned: when Launch Configuration was found - type: str - sample: "ZXhwb3J0IENMT1VE" -name: - description: Name of the Launch Configuration - returned: when Launch Configuration was found - type: str - sample: "myapp-v123" -arn: - description: Name of the AMI - returned: when Launch Configuration was found - type: str - sample: "arn:aws:autoscaling:eu-west-1:12345:launchConfiguration:d82f050e-e315:launchConfigurationName/yourproject" -instance_type: - description: Type of ec2 instance - returned: when Launch Configuration was found - type: str - sample: "t2.small" -created_time: - description: When it was created - returned: when Launch Configuration was found - type: str - sample: "2016-06-29T14:59:22.222000+00:00" -ebs_optimized: - description: Launch Configuration EBS optimized property - returned: when Launch Configuration was found - type: bool - sample: False -instance_monitoring: - description: Launch Configuration instance monitoring property - returned: when Launch Configuration was found - type: str - sample: {"Enabled": false} -classic_link_vpc_security_groups: - description: Launch Configuration classic link vpc security groups property - returned: when Launch Configuration was found - type: list - sample: [] -block_device_mappings: - description: Launch Configuration block device mappings property - returned: when Launch Configuration was found - type: list - sample: [] -keyname: - description: Launch Configuration ssh key - returned: when Launch Configuration was found - type: str - sample: mykey -security_groups: - description: Launch Configuration security groups - returned: when Launch Configuration was found - type: list - sample: [] -kernel_id: - description: Launch Configuration kernel to use - returned: when Launch Configuration was found - type: str - sample: '' -ram_disk_id: - description: Launch Configuration ram disk property - returned: when Launch Configuration was found - type: str - sample: '' -associate_public_address: - description: Assign public address or not - returned: when Launch Configuration was found - type: bool - sample: True -... -''' -import re - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info - - -def find_launch_configs(client, module): - name_regex = module.params.get('name_regex') - sort_order = module.params.get('sort_order') - limit = module.params.get('limit') - - paginator = client.get_paginator('describe_launch_configurations') - - response_iterator = paginator.paginate( - PaginationConfig={ - 'MaxItems': 1000, - 'PageSize': 100 - } - ) - - results = [] - - for response in response_iterator: - response['LaunchConfigurations'] = filter(lambda lc: re.compile(name_regex).match(lc['LaunchConfigurationName']), - response['LaunchConfigurations']) - - for lc in response['LaunchConfigurations']: - data = { - 'name': lc['LaunchConfigurationName'], - 'arn': lc['LaunchConfigurationARN'], - 'created_time': lc['CreatedTime'], - 'user_data': lc['UserData'], - 'instance_type': lc['InstanceType'], - 'image_id': lc['ImageId'], - 'ebs_optimized': lc['EbsOptimized'], - 'instance_monitoring': lc['InstanceMonitoring'], - 'classic_link_vpc_security_groups': lc['ClassicLinkVPCSecurityGroups'], - 'block_device_mappings': lc['BlockDeviceMappings'], - 'keyname': lc['KeyName'], - 'security_groups': lc['SecurityGroups'], - 'kernel_id': lc['KernelId'], - 'ram_disk_id': lc['RamdiskId'], - 'associate_public_address': lc.get('AssociatePublicIpAddress', False), - } - - results.append(data) - - results.sort(key=lambda e: e['name'], reverse=(sort_order == 'descending')) - - if limit: - results = results[:int(limit)] - - module.exit_json(changed=False, results=results) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - name_regex=dict(required=True), - sort_order=dict(required=False, default='ascending', choices=['ascending', 'descending']), - limit=dict(required=False, type='int'), - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - ) - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, True) - - client = boto3_conn(module=module, conn_type='client', resource='autoscaling', region=region, **aws_connect_params) - find_launch_configs(client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_lc_info.py b/lib/ansible/modules/cloud/amazon/ec2_lc_info.py deleted file mode 100644 index 7c963224e0..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_lc_info.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_lc_info -short_description: Gather information about AWS Autoscaling Launch Configurations. -description: - - Gather information about AWS Autoscaling Launch Configurations. - - This module was called C(ec2_lc_facts) before Ansible 2.9. The usage did not change. -version_added: "2.3" -author: "Loïc Latreille (@psykotox)" -requirements: [ boto3 ] -options: - name: - description: - - A name or a list of name to match. - default: [] - type: list - elements: str - sort: - description: - - Optional attribute which with to sort the results. - choices: ['launch_configuration_name', 'image_id', 'created_time', 'instance_type', 'kernel_id', 'ramdisk_id', 'key_name'] - type: str - sort_order: - description: - - Order in which to sort results. - - Only used when the 'sort' parameter is specified. - choices: ['ascending', 'descending'] - default: 'ascending' - type: str - sort_start: - description: - - Which result to start with (when sorting). - - Corresponds to Python slice notation. - type: int - sort_end: - description: - - Which result to end with (when sorting). - - Corresponds to Python slice notation. - type: int -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all launch configurations -- ec2_lc_info: - -# Gather information about launch configuration with name "example" -- ec2_lc_info: - name: example - -# Gather information sorted by created_time from most recent to least recent -- ec2_lc_info: - sort: created_time - sort_order: descending -''' - -RETURN = ''' -block_device_mapping: - description: Block device mapping for the instances of launch configuration - type: list - returned: always - sample: "[{ - 'device_name': '/dev/xvda':, - 'ebs': { - 'delete_on_termination': true, - 'volume_size': 8, - 'volume_type': 'gp2' - }]" -classic_link_vpc_security_groups: - description: IDs of one or more security groups for the VPC specified in classic_link_vpc_id - type: str - returned: always - sample: -created_time: - description: The creation date and time for the launch configuration - type: str - returned: always - sample: "2016-05-27T13:47:44.216000+00:00" -ebs_optimized: - description: EBS I/O optimized (true ) or not (false ) - type: bool - returned: always - sample: true, -image_id: - description: ID of the Amazon Machine Image (AMI) - type: str - returned: always - sample: "ami-12345678" -instance_monitoring: - description: Launched with detailed monitoring or not - type: dict - returned: always - sample: "{ - 'enabled': true - }" -instance_type: - description: Instance type - type: str - returned: always - sample: "t2.micro" -kernel_id: - description: ID of the kernel associated with the AMI - type: str - returned: always - sample: -key_name: - description: Name of the key pair - type: str - returned: always - sample: "user_app" -launch_configuration_arn: - description: Amazon Resource Name (ARN) of the launch configuration - type: str - returned: always - sample: "arn:aws:autoscaling:us-east-1:666612345678:launchConfiguration:ba785e3a-dd42-6f02-4585-ea1a2b458b3d:launchConfigurationName/lc-app" -launch_configuration_name: - description: Name of the launch configuration - type: str - returned: always - sample: "lc-app" -ramdisk_id: - description: ID of the RAM disk associated with the AMI - type: str - returned: always - sample: -security_groups: - description: Security groups to associated - type: list - returned: always - sample: "[ - 'web' - ]" -user_data: - description: User data available - type: str - returned: always - sample: -''' - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info) - - -def list_launch_configs(connection, module): - - launch_config_name = module.params.get("name") - sort = module.params.get('sort') - sort_order = module.params.get('sort_order') - sort_start = module.params.get('sort_start') - sort_end = module.params.get('sort_end') - - try: - pg = connection.get_paginator('describe_launch_configurations') - launch_configs = pg.paginate(LaunchConfigurationNames=launch_config_name).build_full_result() - except ClientError as e: - module.fail_json(msg=e.message) - - snaked_launch_configs = [] - for launch_config in launch_configs['LaunchConfigurations']: - snaked_launch_configs.append(camel_dict_to_snake_dict(launch_config)) - - for launch_config in snaked_launch_configs: - if 'CreatedTime' in launch_config: - launch_config['CreatedTime'] = str(launch_config['CreatedTime']) - - if sort: - snaked_launch_configs.sort(key=lambda e: e[sort], reverse=(sort_order == 'descending')) - - if sort and sort_start and sort_end: - snaked_launch_configs = snaked_launch_configs[sort_start:sort_end] - elif sort and sort_start: - snaked_launch_configs = snaked_launch_configs[sort_start:] - elif sort and sort_end: - snaked_launch_configs = snaked_launch_configs[:sort_end] - - module.exit_json(launch_configurations=snaked_launch_configs) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=False, default=[], type='list'), - sort=dict(required=False, default=None, - choices=['launch_configuration_name', 'image_id', 'created_time', 'instance_type', 'kernel_id', 'ramdisk_id', 'key_name']), - sort_order=dict(required=False, default='ascending', - choices=['ascending', 'descending']), - sort_start=dict(required=False, type='int'), - sort_end=dict(required=False, type='int'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - if module._name == 'ec2_lc_facts': - module.deprecate("The 'ec2_lc_facts' module has been renamed to 'ec2_lc_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='autoscaling', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_launch_configs(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py b/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py deleted file mode 100644 index b221ae49e3..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py +++ /dev/null @@ -1,410 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ -module: ec2_metric_alarm -short_description: "Create/update or delete AWS Cloudwatch 'metric alarms'" -description: - - Can create or delete AWS metric alarms. - - Metrics you wish to alarm on must already exist. -version_added: "1.6" -author: "Zacharie Eakin (@Zeekin)" -options: - state: - description: - - Register or deregister the alarm. - choices: ['present', 'absent'] - default: 'present' - type: str - name: - description: - - Unique name for the alarm. - required: true - type: str - metric: - description: - - Name of the monitored metric (e.g. C(CPUUtilization)). - - Metric must already exist. - required: false - type: str - namespace: - description: - - Name of the appropriate namespace (C(AWS/EC2), C(System/Linux), etc.), which determines the category it will appear under in cloudwatch. - required: false - type: str - statistic: - description: - - Operation applied to the metric. - - Works in conjunction with I(period) and I(evaluation_periods) to determine the comparison value. - required: false - choices: ['SampleCount','Average','Sum','Minimum','Maximum'] - type: str - comparison: - description: - - Determines how the threshold value is compared - - Symbolic comparison operators have been deprecated, and will be removed in 2.14 - required: false - type: str - choices: - - 'GreaterThanOrEqualToThreshold' - - 'GreaterThanThreshold' - - 'LessThanThreshold' - - 'LessThanOrEqualToThreshold' - - '<=' - - '<' - - '>=' - - '>' - threshold: - description: - - Sets the min/max bound for triggering the alarm. - required: false - type: float - period: - description: - - The time (in seconds) between metric evaluations. - required: false - type: int - evaluation_periods: - description: - - The number of times in which the metric is evaluated before final calculation. - required: false - type: int - unit: - description: - - The threshold's unit of measurement. - required: false - type: str - choices: - - 'Seconds' - - 'Microseconds' - - 'Milliseconds' - - 'Bytes' - - 'Kilobytes' - - 'Megabytes' - - 'Gigabytes' - - 'Terabytes' - - 'Bits' - - 'Kilobits' - - 'Megabits' - - 'Gigabits' - - 'Terabits' - - 'Percent' - - 'Count' - - 'Bytes/Second' - - 'Kilobytes/Second' - - 'Megabytes/Second' - - 'Gigabytes/Second' - - 'Terabytes/Second' - - 'Bits/Second' - - 'Kilobits/Second' - - 'Megabits/Second' - - 'Gigabits/Second' - - 'Terabits/Second' - - 'Count/Second' - - 'None' - description: - description: - - A longer description of the alarm. - required: false - type: str - dimensions: - description: - - A dictionary describing which metric the alarm is applied to. - - 'For more information see the AWS documentation:' - - U(https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Dimension) - required: false - type: dict - alarm_actions: - description: - - A list of the names action(s) taken when the alarm is in the C(alarm) status, denoted as Amazon Resource Name(s). - required: false - type: list - elements: str - insufficient_data_actions: - description: - - A list of the names of action(s) to take when the alarm is in the C(insufficient_data) status. - required: false - type: list - elements: str - ok_actions: - description: - - A list of the names of action(s) to take when the alarm is in the C(ok) status, denoted as Amazon Resource Name(s). - required: false - type: list - elements: str - treat_missing_data: - description: - - Sets how the alarm handles missing data points. - required: false - type: str - choices: - - 'breaching' - - 'notBreaching' - - 'ignore' - - 'missing' - default: 'missing' - version_added: "2.10" -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = ''' - - name: create alarm - ec2_metric_alarm: - state: present - region: ap-southeast-2 - name: "cpu-low" - metric: "CPUUtilization" - namespace: "AWS/EC2" - statistic: Average - comparison: "LessThanOrEqualToThreshold" - threshold: 5.0 - period: 300 - evaluation_periods: 3 - unit: "Percent" - description: "This will alarm when a bamboo slave's cpu usage average is lower than 5% for 15 minutes " - dimensions: {'InstanceId':'i-XXX'} - alarm_actions: ["action1","action2"] - - - name: Create an alarm to recover a failed instance - ec2_metric_alarm: - state: present - region: us-west-1 - name: "recover-instance" - metric: "StatusCheckFailed_System" - namespace: "AWS/EC2" - statistic: "Minimum" - comparison: ">=" - threshold: 1.0 - period: 60 - evaluation_periods: 2 - unit: "Count" - description: "This will recover an instance when it fails" - dimensions: {"InstanceId":'i-XXX'} - alarm_actions: ["arn:aws:automate:us-west-1:ec2:recover"] - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # protected by AnsibleAWSModule - - -def create_metric_alarm(connection, module): - - name = module.params.get('name') - metric = module.params.get('metric') - namespace = module.params.get('namespace') - statistic = module.params.get('statistic') - comparison = module.params.get('comparison') - threshold = module.params.get('threshold') - period = module.params.get('period') - evaluation_periods = module.params.get('evaluation_periods') - unit = module.params.get('unit') - description = module.params.get('description') - dimensions = module.params.get('dimensions') - alarm_actions = module.params.get('alarm_actions') - insufficient_data_actions = module.params.get('insufficient_data_actions') - ok_actions = module.params.get('ok_actions') - treat_missing_data = module.params.get('treat_missing_data') - - warnings = [] - - alarms = connection.describe_alarms(AlarmNames=[name]) - - comparisons = {'<=': 'LessThanOrEqualToThreshold', - '<': 'LessThanThreshold', - '>=': 'GreaterThanOrEqualToThreshold', - '>': 'GreaterThanThreshold'} - if comparison in ('<=', '<', '>', '>='): - module.deprecate('Using the <=, <, > and >= operators for comparison has been deprecated. Please use LessThanOrEqualToThreshold, ' - 'LessThanThreshold, GreaterThanThreshold or GreaterThanOrEqualToThreshold instead.', version="2.14") - comparison = comparisons[comparison] - - if not isinstance(dimensions, list): - fixed_dimensions = [] - for key, value in dimensions.items(): - fixed_dimensions.append({'Name': key, 'Value': value}) - dimensions = fixed_dimensions - - if not alarms['MetricAlarms']: - try: - connection.put_metric_alarm(AlarmName=name, - MetricName=metric, - Namespace=namespace, - Statistic=statistic, - ComparisonOperator=comparison, - Threshold=threshold, - Period=period, - EvaluationPeriods=evaluation_periods, - Unit=unit, - AlarmDescription=description, - Dimensions=dimensions, - AlarmActions=alarm_actions, - InsufficientDataActions=insufficient_data_actions, - OKActions=ok_actions, - TreatMissingData=treat_missing_data) - changed = True - alarms = connection.describe_alarms(AlarmNames=[name]) - except ClientError as e: - module.fail_json_aws(e) - - else: - changed = False - alarm = alarms['MetricAlarms'][0] - - # Workaround for alarms created before TreatMissingData was introduced - if 'TreatMissingData' not in alarm.keys(): - alarm['TreatMissingData'] = 'missing' - - for key, value in {'MetricName': metric, - 'Namespace': namespace, - 'Statistic': statistic, - 'ComparisonOperator': comparison, - 'Threshold': threshold, - 'Period': period, - 'EvaluationPeriods': evaluation_periods, - 'Unit': unit, - 'AlarmDescription': description, - 'Dimensions': dimensions, - 'TreatMissingData': treat_missing_data}.items(): - try: - if alarm[key] != value: - changed = True - except KeyError: - if value is not None: - changed = True - - alarm[key] = value - - for key, value in {'AlarmActions': alarm_actions, - 'InsufficientDataActions': insufficient_data_actions, - 'OKActions': ok_actions}.items(): - action = value or [] - if alarm[key] != action: - changed = True - alarm[key] = value - - try: - if changed: - connection.put_metric_alarm(AlarmName=alarm['AlarmName'], - MetricName=alarm['MetricName'], - Namespace=alarm['Namespace'], - Statistic=alarm['Statistic'], - ComparisonOperator=alarm['ComparisonOperator'], - Threshold=alarm['Threshold'], - Period=alarm['Period'], - EvaluationPeriods=alarm['EvaluationPeriods'], - Unit=alarm['Unit'], - AlarmDescription=alarm['AlarmDescription'], - Dimensions=alarm['Dimensions'], - AlarmActions=alarm['AlarmActions'], - InsufficientDataActions=alarm['InsufficientDataActions'], - OKActions=alarm['OKActions'], - TreatMissingData=alarm['TreatMissingData']) - except ClientError as e: - module.fail_json_aws(e) - - result = alarms['MetricAlarms'][0] - module.exit_json(changed=changed, warnings=warnings, - name=result['AlarmName'], - actions_enabled=result['ActionsEnabled'], - alarm_actions=result['AlarmActions'], - alarm_arn=result['AlarmArn'], - comparison=result['ComparisonOperator'], - description=result['AlarmDescription'], - dimensions=result['Dimensions'], - evaluation_periods=result['EvaluationPeriods'], - insufficient_data_actions=result['InsufficientDataActions'], - last_updated=result['AlarmConfigurationUpdatedTimestamp'], - metric=result['MetricName'], - namespace=result['Namespace'], - ok_actions=result['OKActions'], - period=result['Period'], - state_reason=result['StateReason'], - state_value=result['StateValue'], - statistic=result['Statistic'], - threshold=result['Threshold'], - treat_missing_data=result['TreatMissingData'], - unit=result['Unit']) - - -def delete_metric_alarm(connection, module): - name = module.params.get('name') - alarms = connection.describe_alarms(AlarmNames=[name]) - - if alarms['MetricAlarms']: - try: - connection.delete_alarms(AlarmNames=[name]) - module.exit_json(changed=True) - except (ClientError) as e: - module.fail_json_aws(e) - else: - module.exit_json(changed=False) - - -def main(): - argument_spec = dict( - name=dict(required=True, type='str'), - metric=dict(type='str'), - namespace=dict(type='str'), - statistic=dict(type='str', choices=['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum']), - comparison=dict(type='str', choices=['LessThanOrEqualToThreshold', 'LessThanThreshold', 'GreaterThanThreshold', - 'GreaterThanOrEqualToThreshold', '<=', '<', '>', '>=']), - threshold=dict(type='float'), - period=dict(type='int'), - unit=dict(type='str', choices=['Seconds', 'Microseconds', 'Milliseconds', 'Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', - 'Terabytes', 'Bits', 'Kilobits', 'Megabits', 'Gigabits', 'Terabits', 'Percent', 'Count', - 'Bytes/Second', 'Kilobytes/Second', 'Megabytes/Second', 'Gigabytes/Second', - 'Terabytes/Second', 'Bits/Second', 'Kilobits/Second', 'Megabits/Second', 'Gigabits/Second', - 'Terabits/Second', 'Count/Second', 'None']), - evaluation_periods=dict(type='int'), - description=dict(type='str'), - dimensions=dict(type='dict', default={}), - alarm_actions=dict(type='list', default=[]), - insufficient_data_actions=dict(type='list', default=[]), - ok_actions=dict(type='list', default=[]), - treat_missing_data=dict(type='str', choices=['breaching', 'notBreaching', 'ignore', 'missing'], default='missing'), - state=dict(default='present', choices=['present', 'absent']), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - - state = module.params.get('state') - - connection = module.client('cloudwatch') - - if state == 'present': - create_metric_alarm(connection, module) - elif state == 'absent': - delete_metric_alarm(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_placement_group.py b/lib/ansible/modules/cloud/amazon/ec2_placement_group.py deleted file mode 100644 index e59c02171a..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_placement_group.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_placement_group -short_description: Create or delete an EC2 Placement Group -description: - - Create an EC2 Placement Group; if the placement group already exists, - nothing is done. Or, delete an existing placement group. If the placement - group is absent, do nothing. See also - U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html) -version_added: "2.5" -author: "Brad Macpherson (@iiibrad)" -options: - name: - description: - - The name for the placement group. - required: true - type: str - state: - description: - - Create or delete placement group. - default: present - choices: [ 'present', 'absent' ] - type: str - strategy: - description: - - Placement group strategy. Cluster will cluster instances into a - low-latency group in a single Availability Zone, while Spread spreads - instances across underlying hardware. - default: cluster - choices: [ 'cluster', 'spread' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide -# for details. - -# Create a placement group. -- ec2_placement_group: - name: my-cluster - state: present - -# Create a Spread placement group. -- ec2_placement_group: - name: my-cluster - state: present - strategy: spread - -# Delete a placement group. -- ec2_placement_group: - name: my-cluster - state: absent - -''' - - -RETURN = ''' -placement_group: - description: Placement group attributes - returned: when state != absent - type: complex - contains: - name: - description: PG name - type: str - sample: my-cluster - state: - description: PG state - type: str - sample: "available" - strategy: - description: PG strategy - type: str - sample: "cluster" - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry -try: - from botocore.exceptions import (BotoCoreError, ClientError) -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.exponential_backoff() -def get_placement_group_details(connection, module): - name = module.params.get("name") - try: - response = connection.describe_placement_groups( - Filters=[{ - "Name": "group-name", - "Values": [name] - }]) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws( - e, - msg="Couldn't find placement group named [%s]" % name) - - if len(response['PlacementGroups']) != 1: - return None - else: - placement_group = response['PlacementGroups'][0] - return { - "name": placement_group['GroupName'], - "state": placement_group['State'], - "strategy": placement_group['Strategy'], - } - - -@AWSRetry.exponential_backoff() -def create_placement_group(connection, module): - name = module.params.get("name") - strategy = module.params.get("strategy") - - try: - connection.create_placement_group( - GroupName=name, Strategy=strategy, DryRun=module.check_mode) - except (BotoCoreError, ClientError) as e: - if e.response['Error']['Code'] == "DryRunOperation": - module.exit_json(changed=True, placement_group={ - "name": name, - "state": 'DryRun', - "strategy": strategy, - }) - module.fail_json_aws( - e, - msg="Couldn't create placement group [%s]" % name) - - module.exit_json(changed=True, - placement_group=get_placement_group_details( - connection, module - )) - - -@AWSRetry.exponential_backoff() -def delete_placement_group(connection, module): - name = module.params.get("name") - - try: - connection.delete_placement_group( - GroupName=name, DryRun=module.check_mode) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws( - e, - msg="Couldn't delete placement group [%s]" % name) - - module.exit_json(changed=True) - - -def main(): - argument_spec = dict( - name=dict(required=True, type='str'), - state=dict(default='present', choices=['present', 'absent']), - strategy=dict(default='cluster', choices=['cluster', 'spread']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - connection = module.client('ec2') - - state = module.params.get("state") - - if state == 'present': - placement_group = get_placement_group_details(connection, module) - if placement_group is None: - create_placement_group(connection, module) - else: - strategy = module.params.get("strategy") - if placement_group['strategy'] == strategy: - module.exit_json( - changed=False, placement_group=placement_group) - else: - name = module.params.get("name") - module.fail_json( - msg=("Placement group '{}' exists, can't change strategy" + - " from '{}' to '{}'").format( - name, - placement_group['strategy'], - strategy)) - - elif state == 'absent': - placement_group = get_placement_group_details(connection, module) - if placement_group is None: - module.exit_json(changed=False) - else: - delete_placement_group(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_placement_group_info.py b/lib/ansible/modules/cloud/amazon/ec2_placement_group_info.py deleted file mode 100644 index 84cf784a71..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_placement_group_info.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_placement_group_info -short_description: List EC2 Placement Group(s) details -description: - - List details of EC2 Placement Group(s). - - This module was called C(ec2_placement_group_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -author: "Brad Macpherson (@iiibrad)" -options: - names: - description: - - A list of names to filter on. If a listed group does not exist, there - will be no corresponding entry in the result; no error will be raised. - type: list - elements: str - required: false - default: [] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details or the AWS region, -# see the AWS Guide for details. - -# List all placement groups. -- ec2_placement_group_info: - register: all_ec2_placement_groups - -# List two placement groups. -- ec2_placement_group_info: - names: - - my-cluster - - my-other-cluster - register: specific_ec2_placement_groups - -- debug: msg="{{ specific_ec2_placement_groups | json_query(\"[?name=='my-cluster']\") }}" - -''' - - -RETURN = ''' -placement_groups: - description: Placement group attributes - returned: always - type: complex - contains: - name: - description: PG name - type: str - sample: my-cluster - state: - description: PG state - type: str - sample: "available" - strategy: - description: PG strategy - type: str - sample: "cluster" - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -try: - from botocore.exceptions import (BotoCoreError, ClientError) -except ImportError: - pass # caught by AnsibleAWSModule - - -def get_placement_groups_details(connection, module): - names = module.params.get("names") - try: - if len(names) > 0: - response = connection.describe_placement_groups( - Filters=[{ - "Name": "group-name", - "Values": names - }]) - else: - response = connection.describe_placement_groups() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws( - e, - msg="Couldn't find placement groups named [%s]" % names) - - results = [] - for placement_group in response['PlacementGroups']: - results.append({ - "name": placement_group['GroupName'], - "state": placement_group['State'], - "strategy": placement_group['Strategy'], - }) - return results - - -def main(): - argument_spec = dict( - names=dict(type='list', default=[]) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - if module._module._name == 'ec2_placement_group_facts': - module._module.deprecate("The 'ec2_placement_group_facts' module has been renamed to 'ec2_placement_group_info'", version='2.13') - - connection = module.client('ec2') - - placement_groups = get_placement_groups_details(connection, module) - module.exit_json(changed=False, placement_groups=placement_groups) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py b/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py deleted file mode 100644 index 42241882ad..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ -module: ec2_scaling_policy -short_description: Create or delete AWS scaling policies for Autoscaling groups -description: - - Can create or delete scaling policies for autoscaling groups. - - Referenced autoscaling groups must already exist. -version_added: "1.6" -author: "Zacharie Eakin (@Zeekin)" -options: - state: - description: - - Register or deregister the policy. - default: present - choices: ['present', 'absent'] - type: str - name: - description: - - Unique name for the scaling policy. - required: true - type: str - asg_name: - description: - - Name of the associated autoscaling group. - required: true - type: str - adjustment_type: - description: - - The type of change in capacity of the autoscaling group. - choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity'] - type: str - scaling_adjustment: - description: - - The amount by which the autoscaling group is adjusted by the policy. - type: int - min_adjustment_step: - description: - - Minimum amount of adjustment when policy is triggered. - type: int - cooldown: - description: - - The minimum period of time (in seconds) between which autoscaling actions can take place. - type: int -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = ''' -- ec2_scaling_policy: - state: present - region: US-XXX - name: "scaledown-policy" - adjustment_type: "ChangeInCapacity" - asg_name: "slave-pool" - scaling_adjustment: -1 - min_adjustment_step: 1 - cooldown: 300 -''' - -try: - import boto.ec2.autoscale - import boto.exception - from boto.ec2.autoscale import ScalingPolicy - from boto.exception import BotoServerError -except ImportError: - pass # Taken care of by ec2.HAS_BOTO - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (AnsibleAWSError, HAS_BOTO, connect_to_aws, ec2_argument_spec, - get_aws_connection_info) - - -def create_scaling_policy(connection, module): - sp_name = module.params.get('name') - adjustment_type = module.params.get('adjustment_type') - asg_name = module.params.get('asg_name') - scaling_adjustment = module.params.get('scaling_adjustment') - min_adjustment_step = module.params.get('min_adjustment_step') - cooldown = module.params.get('cooldown') - - scalingPolicies = connection.get_all_policies(as_group=asg_name, policy_names=[sp_name]) - - if not scalingPolicies: - sp = ScalingPolicy( - name=sp_name, - adjustment_type=adjustment_type, - as_name=asg_name, - scaling_adjustment=scaling_adjustment, - min_adjustment_step=min_adjustment_step, - cooldown=cooldown) - - try: - connection.create_scaling_policy(sp) - policy = connection.get_all_policies(as_group=asg_name, policy_names=[sp_name])[0] - module.exit_json(changed=True, name=policy.name, arn=policy.policy_arn, as_name=policy.as_name, scaling_adjustment=policy.scaling_adjustment, - cooldown=policy.cooldown, adjustment_type=policy.adjustment_type, min_adjustment_step=policy.min_adjustment_step) - except BotoServerError as e: - module.fail_json(msg=str(e)) - else: - policy = scalingPolicies[0] - changed = False - - # min_adjustment_step attribute is only relevant if the adjustment_type - # is set to percentage change in capacity, so it is a special case - if getattr(policy, 'adjustment_type') == 'PercentChangeInCapacity': - if getattr(policy, 'min_adjustment_step') != module.params.get('min_adjustment_step'): - changed = True - - # set the min adjustment step in case the user decided to change their - # adjustment type to percentage - setattr(policy, 'min_adjustment_step', module.params.get('min_adjustment_step')) - - # check the remaining attributes - for attr in ('adjustment_type', 'scaling_adjustment', 'cooldown'): - if getattr(policy, attr) != module.params.get(attr): - changed = True - setattr(policy, attr, module.params.get(attr)) - - try: - if changed: - connection.create_scaling_policy(policy) - policy = connection.get_all_policies(as_group=asg_name, policy_names=[sp_name])[0] - module.exit_json(changed=changed, name=policy.name, arn=policy.policy_arn, as_name=policy.as_name, scaling_adjustment=policy.scaling_adjustment, - cooldown=policy.cooldown, adjustment_type=policy.adjustment_type, min_adjustment_step=policy.min_adjustment_step) - except BotoServerError as e: - module.fail_json(msg=str(e)) - - -def delete_scaling_policy(connection, module): - sp_name = module.params.get('name') - asg_name = module.params.get('asg_name') - - scalingPolicies = connection.get_all_policies(as_group=asg_name, policy_names=[sp_name]) - - if scalingPolicies: - try: - connection.delete_policy(sp_name, asg_name) - module.exit_json(changed=True) - except BotoServerError as e: - module.exit_json(changed=False, msg=str(e)) - else: - module.exit_json(changed=False) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True, type='str'), - adjustment_type=dict(type='str', choices=['ChangeInCapacity', 'ExactCapacity', 'PercentChangeInCapacity']), - asg_name=dict(required=True, type='str'), - scaling_adjustment=dict(type='int'), - min_adjustment_step=dict(type='int'), - cooldown=dict(type='int'), - state=dict(default='present', choices=['present', 'absent']), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - state = module.params.get('state') - - try: - connection = connect_to_aws(boto.ec2.autoscale, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - - if state == 'present': - create_scaling_policy(connection, module) - elif state == 'absent': - delete_scaling_policy(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_snapshot_copy.py b/lib/ansible/modules/cloud/amazon/ec2_snapshot_copy.py deleted file mode 100644 index 1746a6f2ee..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_snapshot_copy.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - - -DOCUMENTATION = ''' ---- -module: ec2_snapshot_copy -short_description: Copies an EC2 snapshot and returns the new Snapshot ID. -description: - - Copies an EC2 Snapshot from a source region to a destination region. -version_added: "2.4" -options: - source_region: - description: - - The source region the Snapshot should be copied from. - required: true - type: str - source_snapshot_id: - description: - - The ID of the Snapshot in source region that should be copied. - required: true - type: str - description: - description: - - An optional human-readable string describing purpose of the new Snapshot. - type: str - encrypted: - description: - - Whether or not the destination Snapshot should be encrypted. - type: bool - default: 'no' - kms_key_id: - description: - - KMS key id used to encrypt snapshot. If not specified, AWS defaults to C(alias/aws/ebs). - type: str - wait: - description: - - Wait for the copied Snapshot to be in 'Available' state before returning. - type: bool - default: 'no' - wait_timeout: - version_added: "2.6" - description: - - How long before wait gives up, in seconds. - default: 600 - type: int - tags: - description: - - A hash/dictionary of tags to add to the new Snapshot; '{"key":"value"}' and '{"key":"value","key":"value"}' - type: dict -author: Deepak Kothandan (@Deepakkothandan) <deepak.kdy@gmail.com> -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 -''' - -EXAMPLES = ''' -# Basic Snapshot Copy -- ec2_snapshot_copy: - source_region: eu-central-1 - region: eu-west-1 - source_snapshot_id: snap-xxxxxxx - -# Copy Snapshot and wait until available -- ec2_snapshot_copy: - source_region: eu-central-1 - region: eu-west-1 - source_snapshot_id: snap-xxxxxxx - wait: yes - wait_timeout: 1200 # Default timeout is 600 - register: snapshot_id - -# Tagged Snapshot copy -- ec2_snapshot_copy: - source_region: eu-central-1 - region: eu-west-1 - source_snapshot_id: snap-xxxxxxx - tags: - Name: Snapshot-Name - -# Encrypted Snapshot copy -- ec2_snapshot_copy: - source_region: eu-central-1 - region: eu-west-1 - source_snapshot_id: snap-xxxxxxx - encrypted: yes - -# Encrypted Snapshot copy with specified key -- ec2_snapshot_copy: - source_region: eu-central-1 - region: eu-west-1 - source_snapshot_id: snap-xxxxxxx - encrypted: yes - kms_key_id: arn:aws:kms:eu-central-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b -''' - -RETURN = ''' -snapshot_id: - description: snapshot id of the newly created snapshot - returned: when snapshot copy is successful - type: str - sample: "snap-e9095e8c" -''' - -import traceback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, get_aws_connection_info, camel_dict_to_snake_dict) -from ansible.module_utils._text import to_native - -try: - import boto3 - from botocore.exceptions import ClientError, WaiterError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - - -def copy_snapshot(module, ec2): - """ - Copies an EC2 Snapshot to another region - - module : AnsibleModule object - ec2: ec2 connection object - """ - - params = { - 'SourceRegion': module.params.get('source_region'), - 'SourceSnapshotId': module.params.get('source_snapshot_id'), - 'Description': module.params.get('description') - } - - if module.params.get('encrypted'): - params['Encrypted'] = True - - if module.params.get('kms_key_id'): - params['KmsKeyId'] = module.params.get('kms_key_id') - - try: - snapshot_id = ec2.copy_snapshot(**params)['SnapshotId'] - if module.params.get('wait'): - delay = 15 - # Add one to max_attempts as wait() increment - # its counter before assessing it for time.sleep() - max_attempts = (module.params.get('wait_timeout') // delay) + 1 - ec2.get_waiter('snapshot_completed').wait( - SnapshotIds=[snapshot_id], - WaiterConfig=dict(Delay=delay, MaxAttempts=max_attempts) - ) - if module.params.get('tags'): - ec2.create_tags( - Resources=[snapshot_id], - Tags=[{'Key': k, 'Value': v} for k, v in module.params.get('tags').items()] - ) - - except WaiterError as we: - module.fail_json(msg='An error occurred waiting for the snapshot to become available. (%s)' % str(we), exception=traceback.format_exc()) - except ClientError as ce: - module.fail_json(msg=str(ce), exception=traceback.format_exc(), **camel_dict_to_snake_dict(ce.response)) - - module.exit_json(changed=True, snapshot_id=snapshot_id) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - source_region=dict(required=True), - source_snapshot_id=dict(required=True), - description=dict(default=''), - encrypted=dict(type='bool', default=False, required=False), - kms_key_id=dict(type='str', required=False), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=600), - tags=dict(type='dict'))) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO3: - module.fail_json(msg='botocore and boto3 are required.') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ec2', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - - copy_snapshot(module, client) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_transit_gateway.py b/lib/ansible/modules/cloud/amazon/ec2_transit_gateway.py deleted file mode 100644 index 69eadef0e8..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_transit_gateway.py +++ /dev/null @@ -1,578 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_transit_gateway -short_description: Create and delete AWS Transit Gateways -description: - - Creates AWS Transit Gateways. - - Deletes AWS Transit Gateways. - - Updates tags on existing transit gateways. -version_added: "2.8" -requirements: [ 'botocore', 'boto3' ] -options: - asn: - description: - - A private Autonomous System Number (ASN) for the Amazon side of a BGP session. - - The range is 64512 to 65534 for 16-bit ASNs and 4200000000 to 4294967294 for 32-bit ASNs. - type: int - auto_associate: - description: - - Enable or disable automatic association with the default association route table. - default: true - type: bool - auto_attach: - description: - - Enable or disable automatic acceptance of attachment requests. - default: false - type: bool - auto_propagate: - description: - - Enable or disable automatic propagation of routes to the default propagation route table. - default: true - type: bool - description: - description: - - The description of the transit gateway. - type: str - dns_support: - description: - - Whether to enable AWS DNS support. - default: true - type: bool - purge_tags: - description: - - Whether to purge existing tags not included with tags argument. - default: true - type: bool - state: - description: - - C(present) to ensure resource is created. - - C(absent) to remove resource. - default: present - choices: [ "present", "absent"] - type: str - tags: - description: - - A dictionary of resource tags - type: dict - transit_gateway_id: - description: - - The ID of the transit gateway. - type: str - vpn_ecmp_support: - description: - - Enable or disable Equal Cost Multipath Protocol support. - default: true - type: bool - wait: - description: - - Whether to wait for status - default: true - type: bool - wait_timeout: - description: - - number of seconds to wait for status - default: 300 - type: int - -author: "Bob Boldin (@BobBoldin)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create a new transit gateway using defaults - ec2_transit_gateway: - state: present - region: us-east-1 - description: personal-testing - register: created_tgw - -- name: Create a new transit gateway with options - ec2_transit_gateway: - asn: 64514 - auto_associate: no - auto_propagate: no - dns_support: True - description: "nonprod transit gateway" - purge_tags: False - state: present - region: us-east-1 - tags: - Name: nonprod transit gateway - status: testing - -- name: Remove a transit gateway by description - ec2_transit_gateway: - state: absent - region: us-east-1 - description: personal-testing - -- name: Remove a transit gateway by id - ec2_transit_gateway: - state: absent - region: ap-southeast-2 - transit_gateway_id: tgw-3a9aa123 - register: deleted_tgw -''' - -RETURN = ''' -transit_gateway: - description: The attributes of the transit gateway. - type: complex - returned: I(state=present) - contains: - creation_time: - description: The creation time of the transit gateway. - returned: always - type: str - sample: "2019-03-06T17:13:51+00:00" - description: - description: The description of the transit gateway. - returned: always - type: str - sample: my test tgw - options: - description: The options attributes of the transit gateway - returned: always - type: complex - contains: - amazon_side_asn: - description: - - A private Autonomous System Number (ASN) for the Amazon side of a BGP session. - The range is 64512 to 65534 for 16-bit ASNs and 4200000000 to 4294967294 for 32-bit ASNs. - returned: always - type: str - sample: 64512 - auto_accept_shared_attachements: - description: Indicates whether attachment requests are automatically accepted. - returned: always - type: str - sample: disable - default_route_table_association: - description: - - Indicates whether resource attachments are automatically - associated with the default association route table. - returned: always - type: str - sample: enable - association_default_route_table_id: - description: The ID of the default association route table. - returned: Iwhen exists - type: str - sample: tgw-rtb-abc123444 - default_route_table_propagation: - description: - - Indicates whether resource attachments automatically - propagate routes to the default propagation route table. - returned: always - type: str - sample: disable - propagation_default_route_table_id: - description: The ID of the default propagation route table. - returned: when exists - type: str - sample: tgw-rtb-def456777 - vpn_ecmp_support: - description: Indicates whether Equal Cost Multipath Protocol support is enabled. - returned: always - type: str - sample: enable - dns_support: - description: Indicates whether DNS support is enabled. - returned: always - type: str - sample: enable - owner_id: - description: The account that owns the transit gateway. - returned: always - type: str - sample: '123456789012' - state: - description: The state of the transit gateway. - returned: always - type: str - sample: pending - tags: - description: A dictionary of resource tags - returned: always - type: dict - sample: - tags: - Name: nonprod_tgw - transit_gateway_arn: - description: The ID of the transit_gateway. - returned: always - type: str - sample: tgw-3a9aa123 - transit_gateway_id: - description: The ID of the transit_gateway. - returned: always - type: str - sample: tgw-3a9aa123 -''' - -try: - from botocore.exceptions import BotoCoreError, ClientError -except Exception: - pass - # handled by imported AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from time import sleep, time -from ansible.module_utils._text import to_text -from ansible.module_utils.ec2 import ( - ansible_dict_to_boto3_tag_list, - ansible_dict_to_boto3_filter_list, - AWSRetry, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - compare_aws_tags -) - - -class AnsibleEc2Tgw(object): - - def __init__(self, module, results): - self._module = module - self._results = results - self._connection = self._module.client('ec2') - self._check_mode = self._module.check_mode - - if not hasattr(self._connection, 'describe_transit_gateways'): - self._module.fail_json(msg='transit gateway module requires boto3 >= 1.9.52') - - def process(self): - """ Process the request based on state parameter . - state = present will search for an existing tgw based and return the object data. - if no object is found it will be created - - state = absent will attempt to remove the tgw however will fail if it still has - attachments or associations - """ - description = self._module.params.get('description') - state = self._module.params.get('state', 'present') - tgw_id = self._module.params.get('transit_gateway_id') - - if state == 'present': - self.ensure_tgw_present(tgw_id, description) - elif state == 'absent': - self.ensure_tgw_absent(tgw_id, description) - - def wait_for_status(self, wait_timeout, tgw_id, status, skip_deleted=True): - """ - Wait for the Transit Gateway to reach the specified status - :param wait_timeout: Number of seconds to wait, until this timeout is reached. - :param tgw_id: The Amazon nat id. - :param status: The status to wait for. - examples. status=available, status=deleted - :param skip_deleted: ignore deleted transit gateways - :return dict: transit gateway object - """ - polling_increment_secs = 5 - wait_timeout = time() + wait_timeout - status_achieved = False - transit_gateway = dict() - - while wait_timeout > time(): - try: - transit_gateway = self.get_matching_tgw(tgw_id=tgw_id, skip_deleted=skip_deleted) - - if transit_gateway: - if self._check_mode: - transit_gateway['state'] = status - - if transit_gateway.get('state') == status: - status_achieved = True - break - - elif transit_gateway.get('state') == 'failed': - break - - else: - sleep(polling_increment_secs) - - except ClientError as e: - self._module.fail_json_aws(e) - - if not status_achieved: - self._module.fail_json( - msg="Wait time out reached, while waiting for results") - - return transit_gateway - - def get_matching_tgw(self, tgw_id, description=None, skip_deleted=True): - """ search for an existing tgw by either tgw_id or description - :param tgw_id: The AWS id of the transit gateway - :param description: The description of the transit gateway. - :param skip_deleted: ignore deleted transit gateways - :return dict: transit gateway object - """ - filters = [] - if tgw_id: - filters = ansible_dict_to_boto3_filter_list({'transit-gateway-id': tgw_id}) - - try: - response = AWSRetry.exponential_backoff()(self._connection.describe_transit_gateways)(Filters=filters) - except (ClientError, BotoCoreError) as e: - self._module.fail_json_aws(e) - - tgw = None - tgws = [] - - if len(response.get('TransitGateways', [])) == 1 and tgw_id: - if (response['TransitGateways'][0]['State'] != 'deleted') or not skip_deleted: - tgws.extend(response['TransitGateways']) - - for gateway in response.get('TransitGateways', []): - if description == gateway['Description'] and gateway['State'] != 'deleted': - tgws.append(gateway) - - if len(tgws) > 1: - self._module.fail_json( - msg='EC2 returned more than one transit Gateway for description {0}, aborting'.format(description)) - elif tgws: - tgw = camel_dict_to_snake_dict(tgws[0], ignore_list=['Tags']) - tgw['tags'] = boto3_tag_list_to_ansible_dict(tgws[0]['Tags']) - - return tgw - - @staticmethod - def enable_option_flag(flag): - disabled = "disable" - enabled = "enable" - if flag: - return enabled - return disabled - - def create_tgw(self, description): - """ - Create a transit gateway and optionally wait for status to become available. - - :param description: The description of the transit gateway. - :return dict: transit gateway object - """ - options = dict() - wait = self._module.params.get('wait') - wait_timeout = self._module.params.get('wait_timeout') - - if self._module.params.get('asn'): - options['AmazonSideAsn'] = self._module.params.get('asn') - - options['AutoAcceptSharedAttachments'] = self.enable_option_flag(self._module.params.get('auto_attach')) - options['DefaultRouteTableAssociation'] = self.enable_option_flag(self._module.params.get('auto_associate')) - options['DefaultRouteTablePropagation'] = self.enable_option_flag(self._module.params.get('auto_propagate')) - options['VpnEcmpSupport'] = self.enable_option_flag(self._module.params.get('vpn_ecmp_support')) - options['DnsSupport'] = self.enable_option_flag(self._module.params.get('dns_support')) - - try: - response = self._connection.create_transit_gateway(Description=description, Options=options) - except (ClientError, BotoCoreError) as e: - self._module.fail_json_aws(e) - - tgw_id = response['TransitGateway']['TransitGatewayId'] - - if wait: - result = self.wait_for_status(wait_timeout=wait_timeout, tgw_id=tgw_id, status="available") - else: - result = self.get_matching_tgw(tgw_id=tgw_id) - - self._results['msg'] = (' Transit gateway {0} created'.format(result['transit_gateway_id'])) - - return result - - def delete_tgw(self, tgw_id): - """ - De;lete the transit gateway and optionally wait for status to become deleted - - :param tgw_id: The id of the transit gateway - :return dict: transit gateway object - """ - wait = self._module.params.get('wait') - wait_timeout = self._module.params.get('wait_timeout') - - try: - response = self._connection.delete_transit_gateway(TransitGatewayId=tgw_id) - except (ClientError, BotoCoreError) as e: - self._module.fail_json_aws(e) - - if wait: - result = self.wait_for_status(wait_timeout=wait_timeout, tgw_id=tgw_id, status="deleted", skip_deleted=False) - else: - result = self.get_matching_tgw(tgw_id=tgw_id, skip_deleted=False) - - self._results['msg'] = (' Transit gateway {0} deleted'.format(tgw_id)) - - return 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_aws(e, msg="Couldn't describe tags") - - to_update, to_delete = compare_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_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_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_tgw_present(self, tgw_id=None, description=None): - """ - Will create a tgw if no match to the tgw_id or description are found - Will update the tgw tags if matching one found but tags are not synced - - :param tgw_id: The AWS id of the transit gateway - :param description: The description of the transit gateway. - :return dict: transit gateway object - """ - tgw = self.get_matching_tgw(tgw_id, description) - - if tgw is None: - if self._check_mode: - self._results['changed'] = True - self._results['transit_gateway_id'] = None - return self._results - - try: - if not description: - self._module.fail_json(msg="Failed to create Transit Gateway: description argument required") - tgw = self.create_tgw(description) - self._results['changed'] = True - except (BotoCoreError, ClientError) as e: - self._module.fail_json_aws(e, msg='Unable to create Transit Gateway') - - if self._module.params.get('tags') != tgw.get('tags'): - stringed_tags_dict = dict((to_text(k), to_text(v)) for k, v in self._module.params.get('tags').items()) - if self.ensure_tags(tgw['transit_gateway_id'], stringed_tags_dict, self._module.params.get('purge_tags')): - self._results['changed'] = True - - self._results['transit_gateway'] = self.get_matching_tgw(tgw_id=tgw['transit_gateway_id']) - - return self._results - - def ensure_tgw_absent(self, tgw_id=None, description=None): - """ - Will delete the tgw if a single tgw is found not yet in deleted status - - :param tgw_id: The AWS id of the transit gateway - :param description: The description of the transit gateway. - :return doct: transit gateway object - """ - self._results['transit_gateway_id'] = None - tgw = self.get_matching_tgw(tgw_id, description) - - if tgw is not None: - if self._check_mode: - self._results['changed'] = True - return self._results - - try: - tgw = self.delete_tgw(tgw_id=tgw['transit_gateway_id']) - self._results['changed'] = True - self._results['transit_gateway'] = self.get_matching_tgw(tgw_id=tgw['transit_gateway_id'], - skip_deleted=False) - except (BotoCoreError, ClientError) as e: - self._module.fail_json_aws(e, msg='Unable to delete Transit Gateway') - - return self._results - - -def setup_module_object(): - """ - merge argument spec and create Ansible module object - :return: Ansible module object - """ - - argument_spec = dict( - asn=dict(type='int'), - auto_associate=dict(type='bool', default='yes'), - auto_attach=dict(type='bool', default='no'), - auto_propagate=dict(type='bool', default='yes'), - description=dict(type='str'), - dns_support=dict(type='bool', default='yes'), - purge_tags=dict(type='bool', default='yes'), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(default=dict(), type='dict'), - transit_gateway_id=dict(type='str'), - vpn_ecmp_support=dict(type='bool', default='yes'), - wait=dict(type='bool', default='yes'), - wait_timeout=dict(type='int', default=300) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_one_of=[('description', 'transit_gateway_id')], - supports_check_mode=True, - ) - - return module - - -def main(): - - module = setup_module_object() - - results = dict( - changed=False - ) - - tgw_manager = AnsibleEc2Tgw(module=module, results=results) - tgw_manager.process() - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_transit_gateway_info.py b/lib/ansible/modules/cloud/amazon/ec2_transit_gateway_info.py deleted file mode 100644 index 94f86ec11e..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_transit_gateway_info.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = ''' -module: ec2_transit_gateway_info -short_description: Gather information about ec2 transit gateways in AWS -description: - - Gather information about ec2 transit gateways in AWS -version_added: "2.8" -author: "Bob Boldin (@BobBoldin)" -requirements: - - botocore - - boto3 -options: - transit_gateway_ids: - description: - - A list of transit gateway IDs to gather information for. - version_added: "2.8" - aliases: [transit_gateway_id] - type: list - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGateways.html) for filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather info about all transit gateways -- ec2_transit_gateway_info: - -# Gather info about a particular transit gateway using filter transit gateway ID -- ec2_transit_gateway_info: - filters: - transit-gateway-id: tgw-02c42332e6b7da829 - -# Gather info about a particular transit gateway using multiple option filters -- ec2_transit_gateway_info: - filters: - options.dns-support: enable - options.vpn-ecmp-support: enable - -# Gather info about multiple transit gateways using module param -- ec2_transit_gateway_info: - transit_gateway_ids: - - tgw-02c42332e6b7da829 - - tgw-03c53443d5a8cb716 -''' - -RETURN = ''' -transit_gateways: - description: > - Transit gateways that match the provided filters. Each element consists of a dict with all the information - related to that transit gateway. - returned: on success - type: complex - contains: - creation_time: - description: The creation time. - returned: always - type: str - sample: "2019-02-05T16:19:58+00:00" - description: - description: The description of the transit gateway. - returned: always - type: str - sample: "A transit gateway" - options: - description: A dictionary of the transit gateway options. - returned: always - type: complex - contains: - amazon_side_asn: - description: - - A private Autonomous System Number (ASN) for the Amazon - side of a BGP session. The range is 64512 to 65534 for - 16-bit ASNs and 4200000000 to 4294967294 for 32-bit ASNs. - returned: always - type: int - sample: 64512 - auto_accept_shared_attachments: - description: - - Indicates whether attachment requests are automatically accepted. - returned: always - type: str - sample: "enable" - default_route_table_association: - description: - - Indicates whether resource attachments are automatically - associated with the default association route table. - returned: always - type: str - sample: "disable" - association_default_route_table_id: - description: - - The ID of the default association route table. - returned: when present - type: str - sample: "rtb-11223344" - default_route_table_propagation: - description: - - Indicates whether resource attachments automatically - propagate routes to the default propagation route table. - returned: always - type: str - sample: "disable" - dns_support: - description: - - Indicates whether DNS support is enabled. - returned: always - type: str - sample: "enable" - propagation_default_route_table_id: - description: - - The ID of the default propagation route table. - returned: when present - type: str - sample: "rtb-11223344" - vpn_ecmp_support: - description: - - Indicates whether Equal Cost Multipath Protocol support - is enabled. - returned: always - type: str - sample: "enable" - owner_id: - description: The AWS account number ID which owns the transit gateway. - returned: always - type: str - sample: "1234567654323" - state: - description: The state of the transit gateway. - returned: always - type: str - sample: "available" - tags: - description: A dict of tags associated with the transit gateway. - returned: always - type: dict - sample: '{ - "Name": "A sample TGW" - }' - transit_gateway_arn: - description: The Amazon Resource Name (ARN) of the transit gateway. - returned: always - type: str - sample: "arn:aws:ec2:us-west-2:1234567654323:transit-gateway/tgw-02c42332e6b7da829" - transit_gateway_id: - description: The ID of the transit gateway. - returned: always - type: str - sample: "tgw-02c42332e6b7da829" -''' - -try: - from botocore.exceptions import BotoCoreError, ClientError -except Exception: - pass - # handled by imported AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ( - AWSRetry, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - ansible_dict_to_boto3_filter_list -) - - -class AnsibleEc2TgwInfo(object): - - def __init__(self, module, results): - self._module = module - self._results = results - self._connection = self._module.client('ec2') - self._check_mode = self._module.check_mode - - if not hasattr(self._connection, 'describe_transit_gateways'): - self._module.fail_json(msg='transit gateway module requires boto3 >= 1.9.52') - - @AWSRetry.exponential_backoff() - def describe_transit_gateways(self): - """ - Describe transit gateways. - - module : AnsibleAWSModule object - connection : boto3 client connection object - """ - # collect parameters - filters = ansible_dict_to_boto3_filter_list(self._module.params['filters']) - transit_gateway_ids = self._module.params['transit_gateway_ids'] - - # init empty list for return vars - transit_gateway_info = list() - - # Get the basic transit gateway info - try: - response = self._connection.describe_transit_gateways( - TransitGatewayIds=transit_gateway_ids, Filters=filters) - except ClientError as e: - if e.response['Error']['Code'] == 'InvalidTransitGatewayID.NotFound': - self._results['transit_gateways'] = [] - return - raise - - for transit_gateway in response['TransitGateways']: - transit_gateway_info.append(camel_dict_to_snake_dict(transit_gateway, ignore_list=['Tags'])) - # convert tag list to ansible dict - transit_gateway_info[-1]['tags'] = boto3_tag_list_to_ansible_dict(transit_gateway.get('Tags', [])) - - self._results['transit_gateways'] = transit_gateway_info - return - - -def setup_module_object(): - """ - merge argument spec and create Ansible module object - :return: Ansible module object - """ - - argument_spec = dict( - transit_gateway_ids=dict(type='list', default=[], elements='str', aliases=['transit_gateway_id']), - filters=dict(type='dict', default={}) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - return module - - -def main(): - - module = setup_module_object() - - results = dict( - changed=False - ) - - tgwf_manager = AnsibleEc2TgwInfo(module=module, results=results) - try: - tgwf_manager.describe_transit_gateways() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py deleted file mode 100644 index 5e9d587a7c..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_egress_igw -short_description: Manage an AWS VPC Egress Only Internet gateway -description: - - Manage an AWS VPC Egress Only Internet gateway -version_added: "2.5" -author: Daniel Shepherd (@shepdelacreme) -options: - vpc_id: - description: - - The VPC ID for the VPC that this Egress Only Internet Gateway should be attached. - required: true - type: str - state: - description: - - Create or delete the EIGW. - default: present - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Ensure that the VPC has an Internet Gateway. -# The Internet Gateway ID is can be accessed via {{eigw.gateway_id}} for use in setting up NATs etc. -ec2_vpc_egress_igw: - vpc_id: vpc-abcdefgh - state: present -register: eigw - -''' - -RETURN = ''' -gateway_id: - description: The ID of the Egress Only Internet Gateway or Null. - returned: always - type: str - sample: eigw-0e00cf111ba5bc11e -vpc_id: - description: The ID of the VPC to attach or detach gateway from. - returned: always - type: str - sample: vpc-012345678 -''' - - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -def delete_eigw(module, conn, eigw_id): - """ - Delete EIGW. - - module : AnsibleModule object - conn : boto3 client connection object - eigw_id : ID of the EIGW to delete - """ - changed = False - - try: - response = conn.delete_egress_only_internet_gateway(DryRun=module.check_mode, EgressOnlyInternetGatewayId=eigw_id) - except botocore.exceptions.ClientError as e: - # When boto3 method is run with DryRun=True it returns an error on success - # We need to catch the error and return something valid - if e.response.get('Error', {}).get('Code') == "DryRunOperation": - changed = True - else: - module.fail_json_aws(e, msg="Could not delete Egress-Only Internet Gateway {0} from VPC {1}".format(eigw_id, module.vpc_id)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Could not delete Egress-Only Internet Gateway {0} from VPC {1}".format(eigw_id, module.vpc_id)) - - if not module.check_mode: - changed = response.get('ReturnCode', False) - - return changed - - -def create_eigw(module, conn, vpc_id): - """ - Create EIGW. - - module : AnsibleModule object - conn : boto3 client connection object - vpc_id : ID of the VPC we are operating on - """ - gateway_id = None - changed = False - - try: - response = conn.create_egress_only_internet_gateway(DryRun=module.check_mode, VpcId=vpc_id) - except botocore.exceptions.ClientError as e: - # When boto3 method is run with DryRun=True it returns an error on success - # We need to catch the error and return something valid - if e.response.get('Error', {}).get('Code') == "DryRunOperation": - changed = True - elif e.response.get('Error', {}).get('Code') == "InvalidVpcID.NotFound": - module.fail_json_aws(e, msg="invalid vpc ID '{0}' provided".format(vpc_id)) - else: - module.fail_json_aws(e, msg="Could not create Egress-Only Internet Gateway for vpc ID {0}".format(vpc_id)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Could not create Egress-Only Internet Gateway for vpc ID {0}".format(vpc_id)) - - if not module.check_mode: - gateway = response.get('EgressOnlyInternetGateway', {}) - state = gateway.get('Attachments', [{}])[0].get('State') - gateway_id = gateway.get('EgressOnlyInternetGatewayId') - - if gateway_id and state in ('attached', 'attaching'): - changed = True - else: - # EIGW gave back a bad attachment state or an invalid response so we error out - module.fail_json(msg='Unable to create and attach Egress Only Internet Gateway to VPCId: {0}. Bad or no state in response'.format(vpc_id), - **camel_dict_to_snake_dict(response)) - - return changed, gateway_id - - -def describe_eigws(module, conn, vpc_id): - """ - Describe EIGWs. - - module : AnsibleModule object - conn : boto3 client connection object - vpc_id : ID of the VPC we are operating on - """ - gateway_id = None - - try: - response = conn.describe_egress_only_internet_gateways() - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Could not get list of existing Egress-Only Internet Gateways") - - for eigw in response.get('EgressOnlyInternetGateways', []): - for attachment in eigw.get('Attachments', []): - if attachment.get('VpcId') == vpc_id and attachment.get('State') in ('attached', 'attaching'): - gateway_id = eigw.get('EgressOnlyInternetGatewayId') - - return gateway_id - - -def main(): - argument_spec = dict( - vpc_id=dict(required=True), - state=dict(default='present', choices=['present', 'absent']) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - connection = module.client('ec2') - - vpc_id = module.params.get('vpc_id') - state = module.params.get('state') - - eigw_id = describe_eigws(module, connection, vpc_id) - - result = dict(gateway_id=eigw_id, vpc_id=vpc_id) - changed = False - - if state == 'present' and not eigw_id: - changed, result['gateway_id'] = create_eigw(module, connection, vpc_id) - elif state == 'absent' and eigw_id: - changed = delete_eigw(module, connection, eigw_id) - - module.exit_json(changed=changed, **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint.py deleted file mode 100644 index 3d6b3fb8f3..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_vpc_endpoint -short_description: Create and delete AWS VPC Endpoints. -description: - - Creates AWS VPC endpoints. - - Deletes AWS VPC endpoints. - - This module supports check mode. -version_added: "2.4" -requirements: [ boto3 ] -options: - vpc_id: - description: - - Required when creating a VPC endpoint. - required: false - type: str - service: - description: - - An AWS supported vpc endpoint service. Use the M(ec2_vpc_endpoint_info) - module to describe the supported endpoint services. - - Required when creating an endpoint. - required: false - type: str - policy: - description: - - A properly formatted json policy as string, see - U(https://github.com/ansible/ansible/issues/7005#issuecomment-42894813). - Cannot be used with I(policy_file). - - Option when creating an endpoint. If not provided AWS will - utilise a default policy which provides full access to the service. - required: false - type: json - policy_file: - description: - - The path to the properly json formatted policy file, see - U(https://github.com/ansible/ansible/issues/7005#issuecomment-42894813) - on how to use it properly. Cannot be used with I(policy). - - Option when creating an endpoint. If not provided AWS will - utilise a default policy which provides full access to the service. - required: false - aliases: [ "policy_path" ] - type: path - state: - description: - - present to ensure resource is created. - - absent to remove resource - required: false - default: present - choices: [ "present", "absent"] - type: str - wait: - description: - - When specified, will wait for either available status for state present. - Unfortunately this is ignored for delete actions due to a difference in - behaviour from AWS. - required: false - default: no - type: bool - wait_timeout: - description: - - Used in conjunction with wait. Number of seconds to wait for status. - Unfortunately this is ignored for delete actions due to a difference in - behaviour from AWS. - required: false - default: 320 - type: int - route_table_ids: - description: - - List of one or more route table ids to attach to the endpoint. A route - is added to the route table with the destination of the endpoint if - provided. - required: false - type: list - elements: str - vpc_endpoint_id: - description: - - One or more vpc endpoint ids to remove from the AWS account - required: false - type: str - client_token: - description: - - Optional client token to ensure idempotency - required: false - type: str -author: Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Create new vpc endpoint with a json template for policy - ec2_vpc_endpoint: - state: present - region: ap-southeast-2 - vpc_id: vpc-12345678 - service: com.amazonaws.ap-southeast-2.s3 - policy: " {{ lookup( 'template', 'endpoint_policy.json.j2') }} " - route_table_ids: - - rtb-12345678 - - rtb-87654321 - register: new_vpc_endpoint - -- name: Create new vpc endpoint with the default policy - ec2_vpc_endpoint: - state: present - region: ap-southeast-2 - vpc_id: vpc-12345678 - service: com.amazonaws.ap-southeast-2.s3 - route_table_ids: - - rtb-12345678 - - rtb-87654321 - register: new_vpc_endpoint - -- name: Create new vpc endpoint with json file - ec2_vpc_endpoint: - state: present - region: ap-southeast-2 - vpc_id: vpc-12345678 - service: com.amazonaws.ap-southeast-2.s3 - policy_file: "{{ role_path }}/files/endpoint_policy.json" - route_table_ids: - - rtb-12345678 - - rtb-87654321 - register: new_vpc_endpoint - -- name: Delete newly created vpc endpoint - ec2_vpc_endpoint: - state: absent - vpc_endpoint_id: "{{ new_vpc_endpoint.result['VpcEndpointId'] }}" - region: ap-southeast-2 -''' - -RETURN = ''' -endpoints: - description: The resulting endpoints from the module call - returned: success - type: list - sample: [ - { - "creation_timestamp": "2017-02-20T05:04:15+00:00", - "policy_document": { - "Id": "Policy1450910922815", - "Statement": [ - { - "Action": "s3:*", - "Effect": "Allow", - "Principal": "*", - "Resource": [ - "arn:aws:s3:::*/*", - "arn:aws:s3:::*" - ], - "Sid": "Stmt1450910920641" - } - ], - "Version": "2012-10-17" - }, - "route_table_ids": [ - "rtb-abcd1234" - ], - "service_name": "com.amazonaws.ap-southeast-2.s3", - "vpc_endpoint_id": "vpce-a1b2c3d4", - "vpc_id": "vpc-abbad0d0" - } - ] -''' - -import datetime -import json -import time -import traceback - -try: - import botocore -except ImportError: - pass # will be picked up by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (get_aws_connection_info, boto3_conn, ec2_argument_spec, HAS_BOTO3, - camel_dict_to_snake_dict) -from ansible.module_utils.six import string_types - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def wait_for_status(client, module, resource_id, status): - polling_increment_secs = 15 - max_retries = (module.params.get('wait_timeout') // polling_increment_secs) - status_achieved = False - - for x in range(0, max_retries): - try: - resource = get_endpoints(client, module, resource_id)['VpcEndpoints'][0] - if resource['State'] == status: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - return status_achieved, resource - - -def get_endpoints(client, module, resource_id=None): - params = dict() - if resource_id: - params['VpcEndpointIds'] = [resource_id] - - result = json.loads(json.dumps(client.describe_vpc_endpoints(**params), default=date_handler)) - return result - - -def setup_creation(client, module): - vpc_id = module.params.get('vpc_id') - service_name = module.params.get('service') - - if module.params.get('route_table_ids'): - route_table_ids = module.params.get('route_table_ids') - existing_endpoints = get_endpoints(client, module) - for endpoint in existing_endpoints['VpcEndpoints']: - if endpoint['VpcId'] == vpc_id and endpoint['ServiceName'] == service_name: - sorted_endpoint_rt_ids = sorted(endpoint['RouteTableIds']) - sorted_route_table_ids = sorted(route_table_ids) - if sorted_endpoint_rt_ids == sorted_route_table_ids: - return False, camel_dict_to_snake_dict(endpoint) - - changed, result = create_vpc_endpoint(client, module) - - return changed, json.loads(json.dumps(result, default=date_handler)) - - -def create_vpc_endpoint(client, module): - params = dict() - changed = False - token_provided = False - params['VpcId'] = module.params.get('vpc_id') - params['ServiceName'] = module.params.get('service') - params['DryRun'] = module.check_mode - - if module.params.get('route_table_ids'): - params['RouteTableIds'] = module.params.get('route_table_ids') - - if module.params.get('client_token'): - token_provided = True - request_time = datetime.datetime.utcnow() - params['ClientToken'] = module.params.get('client_token') - - policy = None - if module.params.get('policy'): - try: - policy = json.loads(module.params.get('policy')) - except ValueError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - elif module.params.get('policy_file'): - try: - with open(module.params.get('policy_file'), 'r') as json_data: - policy = json.load(json_data) - except Exception as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - if policy: - params['PolicyDocument'] = json.dumps(policy) - - try: - changed = True - result = camel_dict_to_snake_dict(client.create_vpc_endpoint(**params)['VpcEndpoint']) - if token_provided and (request_time > result['creation_timestamp'].replace(tzinfo=None)): - changed = False - elif module.params.get('wait') and not module.check_mode: - status_achieved, result = wait_for_status(client, module, result['vpc_endpoint_id'], 'available') - if not status_achieved: - module.fail_json(msg='Error waiting for vpc endpoint to become available - please check the AWS console') - except botocore.exceptions.ClientError as e: - if "DryRunOperation" in e.message: - changed = True - result = 'Would have created VPC Endpoint if not in check mode' - elif "IdempotentParameterMismatch" in e.message: - module.fail_json(msg="IdempotentParameterMismatch - updates of endpoints are not allowed by the API") - elif "RouteAlreadyExists" in e.message: - module.fail_json(msg="RouteAlreadyExists for one of the route tables - update is not allowed by the API") - else: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except Exception as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - return changed, result - - -def setup_removal(client, module): - params = dict() - changed = False - params['DryRun'] = module.check_mode - if isinstance(module.params.get('vpc_endpoint_id'), string_types): - params['VpcEndpointIds'] = [module.params.get('vpc_endpoint_id')] - else: - params['VpcEndpointIds'] = module.params.get('vpc_endpoint_id') - try: - result = client.delete_vpc_endpoints(**params)['Unsuccessful'] - if not module.check_mode and (result != []): - module.fail_json(msg=result) - except botocore.exceptions.ClientError as e: - if "DryRunOperation" in e.message: - changed = True - result = 'Would have deleted VPC Endpoint if not in check mode' - else: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except Exception as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - return changed, result - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - vpc_id=dict(), - service=dict(), - policy=dict(type='json'), - policy_file=dict(type='path', aliases=['policy_path']), - state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=320, required=False), - route_table_ids=dict(type='list'), - vpc_endpoint_id=dict(), - client_token=dict(), - ) - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['policy', 'policy_file']], - required_if=[ - ['state', 'present', ['vpc_id', 'service']], - ['state', 'absent', ['vpc_endpoint_id']], - ] - ) - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore and boto3 are required for this module') - - state = module.params.get('state') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - except NameError as e: - # Getting around the get_aws_connection_info boto reliance for region - if "global name 'boto' is not defined" in e.message: - module.params['region'] = botocore.session.get_session().get_config_variable('region') - if not module.params['region']: - module.fail_json(msg="Error - no region provided") - else: - module.fail_json(msg="Can't retrieve connection information - " + str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Failed to connect to AWS due to wrong or missing credentials: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - # Ensure resource is present - if state == 'present': - (changed, results) = setup_creation(ec2, module) - else: - (changed, results) = setup_removal(ec2, module) - - module.exit_json(changed=changed, result=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint_info.py deleted file mode 100644 index 9f1c8f261f..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_endpoint_info.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/python -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: ec2_vpc_endpoint_info -short_description: Retrieves AWS VPC endpoints details using AWS methods. -description: - - Gets various details related to AWS VPC Endpoints. - - This module was called C(ec2_vpc_endpoint_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -options: - query: - description: - - Specifies the query action to take. Services returns the supported - AWS services that can be specified when creating an endpoint. - required: True - choices: - - services - - endpoints - type: str - vpc_endpoint_ids: - description: - - Get details of specific endpoint IDs - type: list - elements: str - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcEndpoints.html) - for possible filters. - type: dict -author: Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple example of listing all support AWS services for VPC endpoints -- name: List supported AWS endpoint services - ec2_vpc_endpoint_info: - query: services - region: ap-southeast-2 - register: supported_endpoint_services - -- name: Get all endpoints in ap-southeast-2 region - ec2_vpc_endpoint_info: - query: endpoints - region: ap-southeast-2 - register: existing_endpoints - -- name: Get all endpoints with specific filters - ec2_vpc_endpoint_info: - query: endpoints - region: ap-southeast-2 - filters: - vpc-id: - - vpc-12345678 - - vpc-87654321 - vpc-endpoint-state: - - available - - pending - register: existing_endpoints - -- name: Get details on specific endpoint - ec2_vpc_endpoint_info: - query: endpoints - region: ap-southeast-2 - vpc_endpoint_ids: - - vpce-12345678 - register: endpoint_details -''' - -RETURN = ''' -service_names: - description: AWS VPC endpoint service names - returned: I(query) is C(services) - type: list - sample: - service_names: - - com.amazonaws.ap-southeast-2.s3 -vpc_endpoints: - description: - - A list of endpoints that match the query. Each endpoint has the keys creation_timestamp, - policy_document, route_table_ids, service_name, state, vpc_endpoint_id, vpc_id. - returned: I(query) is C(endpoints) - type: list - sample: - vpc_endpoints: - - creation_timestamp: "2017-02-16T11:06:48+00:00" - policy_document: > - "{\"Version\":\"2012-10-17\",\"Id\":\"Policy1450910922815\", - \"Statement\":[{\"Sid\":\"Stmt1450910920641\",\"Effect\":\"Allow\", - \"Principal\":\"*\",\"Action\":\"s3:*\",\"Resource\":[\"arn:aws:s3:::*/*\",\"arn:aws:s3:::*\"]}]}" - route_table_ids: - - rtb-abcd1234 - service_name: "com.amazonaws.ap-southeast-2.s3" - state: "available" - vpc_endpoint_id: "vpce-abbad0d0" - vpc_id: "vpc-1111ffff" -''' - -import json - -try: - import botocore -except ImportError: - pass # will be picked up from imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, boto3_conn, get_aws_connection_info, - ansible_dict_to_boto3_filter_list, HAS_BOTO3, camel_dict_to_snake_dict, AWSRetry) - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -@AWSRetry.exponential_backoff() -def get_supported_services(client, module): - results = list() - params = dict() - while True: - response = client.describe_vpc_endpoint_services(**params) - results.extend(response['ServiceNames']) - if 'NextToken' in response: - params['NextToken'] = response['NextToken'] - else: - break - return dict(service_names=results) - - -@AWSRetry.exponential_backoff() -def get_endpoints(client, module): - results = list() - params = dict() - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - if module.params.get('vpc_endpoint_ids'): - params['VpcEndpointIds'] = module.params.get('vpc_endpoint_ids') - while True: - response = client.describe_vpc_endpoints(**params) - results.extend(response['VpcEndpoints']) - if 'NextToken' in response: - params['NextToken'] = response['NextToken'] - else: - break - try: - results = json.loads(json.dumps(results, default=date_handler)) - except Exception as e: - module.fail_json(msg=str(e.message)) - return dict(vpc_endpoints=[camel_dict_to_snake_dict(result) for result in results]) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - query=dict(choices=['services', 'endpoints'], required=True), - filters=dict(default={}, type='dict'), - vpc_endpoint_ids=dict(type='list'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vpc_endpoint_facts': - module.deprecate("The 'ec2_vpc_endpoint_facts' module has been renamed to 'ec2_vpc_endpoint_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore and boto3 are required.') - - try: - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg=str(e)) - - invocations = { - 'services': get_supported_services, - 'endpoints': get_endpoints, - } - results = invocations[module.params.get('query')](connection, module) - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_igw.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_igw.py deleted file mode 100644 index 5198527af7..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_igw.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_igw -short_description: Manage an AWS VPC Internet gateway -description: - - Manage an AWS VPC Internet gateway -version_added: "2.0" -author: Robert Estelle (@erydo) -options: - vpc_id: - description: - - The VPC ID for the VPC in which to manage the Internet Gateway. - required: true - type: str - tags: - description: - - "A dict of tags to apply to the internet gateway. Any tags currently applied to the internet gateway and not present here will be removed." - aliases: [ 'resource_tags' ] - version_added: "2.4" - type: dict - state: - description: - - Create or terminate the IGW - default: present - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -requirements: - - botocore - - boto3 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Ensure that the VPC has an Internet Gateway. -# The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use in setting up NATs etc. -ec2_vpc_igw: - vpc_id: vpc-abcdefgh - state: present -register: igw - -''' - -RETURN = ''' -changed: - description: If any changes have been made to the Internet Gateway. - type: bool - returned: always - sample: - changed: false -gateway_id: - description: The unique identifier for the Internet Gateway. - type: str - returned: I(state=present) - sample: - gateway_id: "igw-XXXXXXXX" -tags: - description: The tags associated the Internet Gateway. - type: dict - returned: I(state=present) - sample: - tags: - "Ansible": "Test" -vpc_id: - description: The VPC ID associated with the Internet Gateway. - type: str - returned: I(state=present) - sample: - vpc_id: "vpc-XXXXXXXX" -''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import ( - AWSRetry, - camel_dict_to_snake_dict, - boto3_tag_list_to_ansible_dict, - ansible_dict_to_boto3_filter_list, - ansible_dict_to_boto3_tag_list, - compare_aws_tags -) -from ansible.module_utils.six import string_types - - -class AnsibleEc2Igw(object): - - def __init__(self, module, results): - self._module = module - self._results = results - self._connection = self._module.client('ec2') - self._check_mode = self._module.check_mode - - def process(self): - vpc_id = self._module.params.get('vpc_id') - state = self._module.params.get('state', 'present') - tags = self._module.params.get('tags') - - if state == 'present': - self.ensure_igw_present(vpc_id, tags) - elif state == 'absent': - self.ensure_igw_absent(vpc_id) - - def get_matching_igw(self, vpc_id): - filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id}) - igws = [] - try: - response = self._connection.describe_internet_gateways(Filters=filters) - igws = response.get('InternetGateways', []) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e) - - igw = None - if len(igws) > 1: - self._module.fail_json( - msg='EC2 returned more than one Internet Gateway for VPC {0}, aborting'.format(vpc_id)) - elif igws: - igw = camel_dict_to_snake_dict(igws[0]) - - return igw - - def check_input_tags(self, tags): - nonstring_tags = [k for k, v in tags.items() if not isinstance(v, string_types)] - if nonstring_tags: - self._module.fail_json(msg='One or more tags contain non-string values: {0}'.format(nonstring_tags)) - - def ensure_tags(self, igw_id, tags, add_only): - final_tags = [] - - filters = ansible_dict_to_boto3_filter_list({'resource-id': igw_id, 'resource-type': 'internet-gateway'}) - cur_tags = None - try: - cur_tags = self._connection.describe_tags(Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't describe tags") - - purge_tags = bool(not add_only) - to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) - final_tags = boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')) - - if to_update: - try: - if self._check_mode: - # update tags - final_tags.update(to_update) - else: - AWSRetry.exponential_backoff()(self._connection.create_tags)( - Resources=[igw_id], - Tags=ansible_dict_to_boto3_tag_list(to_update) - ) - - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't create tags") - - if to_delete: - try: - if self._check_mode: - # update tags - for key in to_delete: - del final_tags[key] - else: - tags_list = [] - for key in to_delete: - tags_list.append({'Key': key}) - - AWSRetry.exponential_backoff()(self._connection.delete_tags)(Resources=[igw_id], Tags=tags_list) - - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't delete tags") - - if not self._check_mode and (to_update or to_delete): - try: - response = self._connection.describe_tags(Filters=filters) - final_tags = boto3_tag_list_to_ansible_dict(response.get('Tags')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't describe tags") - - return final_tags - - @staticmethod - def get_igw_info(igw): - return { - 'gateway_id': igw['internet_gateway_id'], - 'tags': igw['tags'], - 'vpc_id': igw['vpc_id'] - } - - def ensure_igw_absent(self, vpc_id): - igw = self.get_matching_igw(vpc_id) - if igw is None: - return self._results - - if self._check_mode: - self._results['changed'] = True - return self._results - - try: - self._results['changed'] = True - self._connection.detach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id) - self._connection.delete_internet_gateway(InternetGatewayId=igw['internet_gateway_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Unable to delete Internet Gateway") - - return self._results - - def ensure_igw_present(self, vpc_id, tags): - self.check_input_tags(tags) - - igw = self.get_matching_igw(vpc_id) - - if igw is None: - if self._check_mode: - self._results['changed'] = True - self._results['gateway_id'] = None - return self._results - - try: - response = self._connection.create_internet_gateway() - - # Ensure the gateway exists before trying to attach it or add tags - waiter = get_waiter(self._connection, 'internet_gateway_exists') - waiter.wait(InternetGatewayIds=[response['InternetGateway']['InternetGatewayId']]) - - igw = camel_dict_to_snake_dict(response['InternetGateway']) - self._connection.attach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id) - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg='Unable to create Internet Gateway') - - igw['vpc_id'] = vpc_id - - igw['tags'] = self.ensure_tags(igw_id=igw['internet_gateway_id'], tags=tags, add_only=False) - - igw_info = self.get_igw_info(igw) - self._results.update(igw_info) - - return self._results - - -def main(): - argument_spec = dict( - vpc_id=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(default=dict(), required=False, type='dict', aliases=['resource_tags']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - results = dict( - changed=False - ) - igw_manager = AnsibleEc2Igw(module=module, results=results) - igw_manager.process() - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_igw_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_igw_info.py deleted file mode 100644 index 11ee974ae5..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_igw_info.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_igw_info -short_description: Gather information about internet gateways in AWS -description: - - Gather information about internet gateways in AWS. - - This module was called C(ec2_vpc_igw_facts) before Ansible 2.9. The usage did not change. -version_added: "2.3" -requirements: [ boto3 ] -author: "Nick Aslanidis (@naslanidis)" -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInternetGateways.html) for possible filters. - type: dict - internet_gateway_ids: - description: - - Get details of specific Internet Gateway ID. Provide this value as a list. - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# # Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Gather information about all Internet Gateways for an account or profile - ec2_vpc_igw_info: - region: ap-southeast-2 - profile: production - register: igw_info - -- name: Gather information about a filtered list of Internet Gateways - ec2_vpc_igw_info: - region: ap-southeast-2 - profile: production - filters: - "tag:Name": "igw-123" - register: igw_info - -- name: Gather information about a specific internet gateway by InternetGatewayId - ec2_vpc_igw_info: - region: ap-southeast-2 - profile: production - internet_gateway_ids: igw-c1231234 - register: igw_info -''' - -RETURN = ''' -internet_gateways: - description: The internet gateways for the account. - returned: always - type: list - sample: [ - { - "attachments": [ - { - "state": "available", - "vpc_id": "vpc-02123b67" - } - ], - "internet_gateway_id": "igw-2123634d", - "tags": [ - { - "key": "Name", - "value": "test-vpc-20-igw" - } - ] - } - ] - -changed: - description: True if listing the internet gateways succeeds. - type: bool - returned: always - sample: "false" -''' - -try: - import botocore -except ImportError: - pass # will be captured by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, - camel_dict_to_snake_dict, ansible_dict_to_boto3_filter_list, HAS_BOTO3) - - -def get_internet_gateway_info(internet_gateway): - internet_gateway_info = {'InternetGatewayId': internet_gateway['InternetGatewayId'], - 'Attachments': internet_gateway['Attachments'], - 'Tags': internet_gateway['Tags']} - return internet_gateway_info - - -def list_internet_gateways(client, module): - params = dict() - - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - - if module.params.get("internet_gateway_ids"): - params['InternetGatewayIds'] = module.params.get("internet_gateway_ids") - - try: - all_internet_gateways = client.describe_internet_gateways(**params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - return [camel_dict_to_snake_dict(get_internet_gateway_info(igw)) - for igw in all_internet_gateways['InternetGateways']] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(type='dict', default=dict()), - internet_gateway_ids=dict(type='list', default=None) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vpc_igw_facts': - module.deprecate("The 'ec2_vpc_igw_facts' module has been renamed to 'ec2_vpc_igw_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore and boto3 are required.') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - " + str(e)) - - # call your function here - results = list_internet_gateways(connection, module) - - module.exit_json(internet_gateways=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl.py deleted file mode 100644 index 5e8ea95a04..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl.py +++ /dev/null @@ -1,634 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_vpc_nacl -short_description: create and delete Network ACLs. -description: - - Read the AWS documentation for Network ACLS - U(https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_ACLs.html) -version_added: "2.2" -options: - name: - description: - - Tagged name identifying a network ACL. - - One and only one of the I(name) or I(nacl_id) is required. - required: false - type: str - nacl_id: - description: - - NACL id identifying a network ACL. - - One and only one of the I(name) or I(nacl_id) is required. - required: false - version_added: "2.4" - type: str - vpc_id: - description: - - VPC id of the requesting VPC. - - Required when state present. - required: false - type: str - subnets: - description: - - The list of subnets that should be associated with the network ACL. - - Must be specified as a list - - Each subnet can be specified as subnet ID, or its tagged name. - required: false - type: list - egress: - description: - - A list of rules for outgoing traffic. Each rule must be specified as a list. - Each rule may contain the rule number (integer 1-32766), protocol (one of ['tcp', 'udp', 'icmp', '-1', 'all']), - the rule action ('allow' or 'deny') the CIDR of the IPv4 network range to allow or deny, - the ICMP type (-1 means all types), the ICMP code (-1 means all codes), the last port in the range for - TCP or UDP protocols, and the first port in the range for TCP or UDP protocols. - See examples. - default: [] - required: false - type: list - ingress: - description: - - List of rules for incoming traffic. Each rule must be specified as a list. - Each rule may contain the rule number (integer 1-32766), protocol (one of ['tcp', 'udp', 'icmp', '-1', 'all']), - the rule action ('allow' or 'deny') the CIDR of the IPv4 network range to allow or deny, - the ICMP type (-1 means all types), the ICMP code (-1 means all codes), the last port in the range for - TCP or UDP protocols, and the first port in the range for TCP or UDP protocols. - See examples. - default: [] - required: false - type: list - tags: - description: - - Dictionary of tags to look for and apply when creating a network ACL. - required: false - type: dict - state: - description: - - Creates or modifies an existing NACL - - Deletes a NACL and reassociates subnets to the default NACL - required: false - type: str - choices: ['present', 'absent'] - default: present -author: Mike Mochan (@mmochan) -extends_documentation_fragment: - - aws - - ec2 -requirements: [ botocore, boto3, json ] -''' - -EXAMPLES = ''' - -# Complete example to create and delete a network ACL -# that allows SSH, HTTP and ICMP in, and all traffic out. -- name: "Create and associate production DMZ network ACL with DMZ subnets" - ec2_vpc_nacl: - vpc_id: vpc-12345678 - name: prod-dmz-nacl - region: ap-southeast-2 - subnets: ['prod-dmz-1', 'prod-dmz-2'] - tags: - CostCode: CC1234 - Project: phoenix - Description: production DMZ - ingress: - # rule no, protocol, allow/deny, cidr, icmp_type, icmp_code, - # port from, port to - - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] - - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] - - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] - egress: - - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] - state: 'present' - -- name: "Remove the ingress and egress rules - defaults to deny all" - ec2_vpc_nacl: - vpc_id: vpc-12345678 - name: prod-dmz-nacl - region: ap-southeast-2 - subnets: - - prod-dmz-1 - - prod-dmz-2 - tags: - CostCode: CC1234 - Project: phoenix - Description: production DMZ - state: present - -- name: "Remove the NACL subnet associations and tags" - ec2_vpc_nacl: - vpc_id: 'vpc-12345678' - name: prod-dmz-nacl - region: ap-southeast-2 - state: present - -- name: "Delete nacl and subnet associations" - ec2_vpc_nacl: - vpc_id: vpc-12345678 - name: prod-dmz-nacl - state: absent - -- name: "Delete nacl by its id" - ec2_vpc_nacl: - nacl_id: acl-33b4ee5b - state: absent -''' -RETURN = ''' -task: - description: The result of the create, or delete action. - returned: success - type: dict -nacl_id: - description: The id of the NACL (when creating or updating an ACL) - returned: success - type: str - sample: acl-123456789abcdef01 -''' - -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry - -# VPC-supported IANA protocol numbers -# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml -PROTOCOL_NUMBERS = {'all': -1, 'icmp': 1, 'tcp': 6, 'udp': 17, } - - -# Utility methods -def icmp_present(entry): - if len(entry) == 6 and entry[1] == 'icmp' or entry[1] == 1: - return True - - -def load_tags(module): - tags = [] - if module.params.get('tags'): - for name, value in module.params.get('tags').items(): - tags.append({'Key': name, 'Value': str(value)}) - tags.append({'Key': "Name", 'Value': module.params.get('name')}) - else: - tags.append({'Key': "Name", 'Value': module.params.get('name')}) - return tags - - -def subnets_removed(nacl_id, subnets, client, module): - results = find_acl_by_id(nacl_id, client, module) - associations = results['NetworkAcls'][0]['Associations'] - subnet_ids = [assoc['SubnetId'] for assoc in associations] - return [subnet for subnet in subnet_ids if subnet not in subnets] - - -def subnets_added(nacl_id, subnets, client, module): - results = find_acl_by_id(nacl_id, client, module) - associations = results['NetworkAcls'][0]['Associations'] - subnet_ids = [assoc['SubnetId'] for assoc in associations] - return [subnet for subnet in subnets if subnet not in subnet_ids] - - -def subnets_changed(nacl, client, module): - changed = False - vpc_id = module.params.get('vpc_id') - nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] - subnets = subnets_to_associate(nacl, client, module) - if not subnets: - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] - subnets = find_subnet_ids_by_nacl_id(nacl_id, client, module) - if subnets: - replace_network_acl_association(default_nacl_id, subnets, client, module) - changed = True - return changed - changed = False - return changed - subs_added = subnets_added(nacl_id, subnets, client, module) - if subs_added: - replace_network_acl_association(nacl_id, subs_added, client, module) - changed = True - subs_removed = subnets_removed(nacl_id, subnets, client, module) - if subs_removed: - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] - replace_network_acl_association(default_nacl_id, subs_removed, client, module) - changed = True - return changed - - -def nacls_changed(nacl, client, module): - changed = False - params = dict() - params['egress'] = module.params.get('egress') - params['ingress'] = module.params.get('ingress') - - nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] - nacl = describe_network_acl(client, module) - entries = nacl['NetworkAcls'][0]['Entries'] - egress = [rule for rule in entries if rule['Egress'] is True and rule['RuleNumber'] < 32767] - ingress = [rule for rule in entries if rule['Egress'] is False and rule['RuleNumber'] < 32767] - if rules_changed(egress, params['egress'], True, nacl_id, client, module): - changed = True - if rules_changed(ingress, params['ingress'], False, nacl_id, client, module): - changed = True - return changed - - -def tags_changed(nacl_id, client, module): - changed = False - tags = dict() - if module.params.get('tags'): - tags = module.params.get('tags') - if module.params.get('name') and not tags.get('Name'): - tags['Name'] = module.params['name'] - nacl = find_acl_by_id(nacl_id, client, module) - if nacl['NetworkAcls']: - nacl_values = [t.values() for t in nacl['NetworkAcls'][0]['Tags']] - nacl_tags = [item for sublist in nacl_values for item in sublist] - tag_values = [[key, str(value)] for key, value in tags.items()] - tags = [item for sublist in tag_values for item in sublist] - if sorted(nacl_tags) == sorted(tags): - changed = False - return changed - else: - delete_tags(nacl_id, client, module) - create_tags(nacl_id, client, module) - changed = True - return changed - return changed - - -def rules_changed(aws_rules, param_rules, Egress, nacl_id, client, module): - changed = False - rules = list() - for entry in param_rules: - rules.append(process_rule_entry(entry, Egress)) - if rules == aws_rules: - return changed - else: - removed_rules = [x for x in aws_rules if x not in rules] - if removed_rules: - params = dict() - for rule in removed_rules: - params['NetworkAclId'] = nacl_id - params['RuleNumber'] = rule['RuleNumber'] - params['Egress'] = Egress - delete_network_acl_entry(params, client, module) - changed = True - added_rules = [x for x in rules if x not in aws_rules] - if added_rules: - for rule in added_rules: - rule['NetworkAclId'] = nacl_id - create_network_acl_entry(rule, client, module) - changed = True - return changed - - -def process_rule_entry(entry, Egress): - params = dict() - params['RuleNumber'] = entry[0] - params['Protocol'] = str(PROTOCOL_NUMBERS[entry[1]]) - params['RuleAction'] = entry[2] - params['Egress'] = Egress - params['CidrBlock'] = entry[3] - if icmp_present(entry): - params['IcmpTypeCode'] = {"Type": int(entry[4]), "Code": int(entry[5])} - else: - if entry[6] or entry[7]: - params['PortRange'] = {"From": entry[6], 'To': entry[7]} - return params - - -def restore_default_associations(assoc_ids, default_nacl_id, client, module): - if assoc_ids: - params = dict() - params['NetworkAclId'] = default_nacl_id[0] - for assoc_id in assoc_ids: - params['AssociationId'] = assoc_id - restore_default_acl_association(params, client, module) - return True - - -def construct_acl_entries(nacl, client, module): - for entry in module.params.get('ingress'): - params = process_rule_entry(entry, Egress=False) - params['NetworkAclId'] = nacl['NetworkAcl']['NetworkAclId'] - create_network_acl_entry(params, client, module) - for rule in module.params.get('egress'): - params = process_rule_entry(rule, Egress=True) - params['NetworkAclId'] = nacl['NetworkAcl']['NetworkAclId'] - create_network_acl_entry(params, client, module) - - -# Module invocations -def setup_network_acl(client, module): - changed = False - nacl = describe_network_acl(client, module) - if not nacl['NetworkAcls']: - nacl = create_network_acl(module.params.get('vpc_id'), client, module) - nacl_id = nacl['NetworkAcl']['NetworkAclId'] - create_tags(nacl_id, client, module) - subnets = subnets_to_associate(nacl, client, module) - replace_network_acl_association(nacl_id, subnets, client, module) - construct_acl_entries(nacl, client, module) - changed = True - return(changed, nacl['NetworkAcl']['NetworkAclId']) - else: - changed = False - nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] - changed |= subnets_changed(nacl, client, module) - changed |= nacls_changed(nacl, client, module) - changed |= tags_changed(nacl_id, client, module) - return (changed, nacl_id) - - -def remove_network_acl(client, module): - changed = False - result = dict() - nacl = describe_network_acl(client, module) - if nacl['NetworkAcls']: - nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] - vpc_id = nacl['NetworkAcls'][0]['VpcId'] - associations = nacl['NetworkAcls'][0]['Associations'] - assoc_ids = [a['NetworkAclAssociationId'] for a in associations] - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module) - if not default_nacl_id: - result = {vpc_id: "Default NACL ID not found - Check the VPC ID"} - return changed, result - if restore_default_associations(assoc_ids, default_nacl_id, client, module): - delete_network_acl(nacl_id, client, module) - changed = True - result[nacl_id] = "Successfully deleted" - return changed, result - if not assoc_ids: - delete_network_acl(nacl_id, client, module) - changed = True - result[nacl_id] = "Successfully deleted" - return changed, result - return changed, result - - -# Boto3 client methods -@AWSRetry.jittered_backoff() -def _create_network_acl(client, *args, **kwargs): - return client.create_network_acl(*args, **kwargs) - - -def create_network_acl(vpc_id, client, module): - try: - if module.check_mode: - nacl = dict(NetworkAcl=dict(NetworkAclId="nacl-00000000")) - else: - nacl = _create_network_acl(client, VpcId=vpc_id) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return nacl - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _create_network_acl_entry(client, *args, **kwargs): - return client.create_network_acl_entry(*args, **kwargs) - - -def create_network_acl_entry(params, client, module): - try: - if not module.check_mode: - _create_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _create_tags(client, *args, **kwargs): - return client.create_tags(*args, **kwargs) - - -def create_tags(nacl_id, client, module): - try: - delete_tags(nacl_id, client, module) - if not module.check_mode: - _create_tags(client, Resources=[nacl_id], Tags=load_tags(module)) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _delete_network_acl(client, *args, **kwargs): - return client.delete_network_acl(*args, **kwargs) - - -def delete_network_acl(nacl_id, client, module): - try: - if not module.check_mode: - _delete_network_acl(client, NetworkAclId=nacl_id) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _delete_network_acl_entry(client, *args, **kwargs): - return client.delete_network_acl_entry(*args, **kwargs) - - -def delete_network_acl_entry(params, client, module): - try: - if not module.check_mode: - _delete_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _delete_tags(client, *args, **kwargs): - return client.delete_tags(*args, **kwargs) - - -def delete_tags(nacl_id, client, module): - try: - if not module.check_mode: - _delete_tags(client, Resources=[nacl_id]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _describe_network_acls(client, **kwargs): - return client.describe_network_acls(**kwargs) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _describe_network_acls_retry_missing(client, **kwargs): - return client.describe_network_acls(**kwargs) - - -def describe_acl_associations(subnets, client, module): - if not subnets: - return [] - try: - results = _describe_network_acls_retry_missing(client, Filters=[ - {'Name': 'association.subnet-id', 'Values': subnets} - ]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - associations = results['NetworkAcls'][0]['Associations'] - return [a['NetworkAclAssociationId'] for a in associations if a['SubnetId'] in subnets] - - -def describe_network_acl(client, module): - try: - if module.params.get('nacl_id'): - nacl = _describe_network_acls(client, Filters=[ - {'Name': 'network-acl-id', 'Values': [module.params.get('nacl_id')]} - ]) - else: - nacl = _describe_network_acls(client, Filters=[ - {'Name': 'tag:Name', 'Values': [module.params.get('name')]} - ]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return nacl - - -def find_acl_by_id(nacl_id, client, module): - try: - return _describe_network_acls_retry_missing(client, NetworkAclIds=[nacl_id]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -def find_default_vpc_nacl(vpc_id, client, module): - try: - response = _describe_network_acls_retry_missing(client, Filters=[ - {'Name': 'vpc-id', 'Values': [vpc_id]}]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - nacls = response['NetworkAcls'] - return [n['NetworkAclId'] for n in nacls if n['IsDefault'] is True] - - -def find_subnet_ids_by_nacl_id(nacl_id, client, module): - try: - results = _describe_network_acls_retry_missing(client, Filters=[ - {'Name': 'association.network-acl-id', 'Values': [nacl_id]} - ]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - if results['NetworkAcls']: - associations = results['NetworkAcls'][0]['Associations'] - return [s['SubnetId'] for s in associations if s['SubnetId']] - else: - return [] - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _replace_network_acl_association(client, *args, **kwargs): - return client.replace_network_acl_association(*args, **kwargs) - - -def replace_network_acl_association(nacl_id, subnets, client, module): - params = dict() - params['NetworkAclId'] = nacl_id - for association in describe_acl_associations(subnets, client, module): - params['AssociationId'] = association - try: - if not module.check_mode: - _replace_network_acl_association(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _replace_network_acl_entry(client, *args, **kwargs): - return client.replace_network_acl_entry(*args, **kwargs) - - -def replace_network_acl_entry(entries, Egress, nacl_id, client, module): - for entry in entries: - params = entry - params['NetworkAclId'] = nacl_id - try: - if not module.check_mode: - _replace_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidNetworkAclID.NotFound']) -def _replace_network_acl_association(client, *args, **kwargs): - return client.replace_network_acl_association(*args, **kwargs) - - -def restore_default_acl_association(params, client, module): - try: - if not module.check_mode: - _replace_network_acl_association(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _describe_subnets(client, *args, **kwargs): - return client.describe_subnets(*args, **kwargs) - - -def subnets_to_associate(nacl, client, module): - params = list(module.params.get('subnets')) - if not params: - return [] - all_found = [] - if any(x.startswith("subnet-") for x in params): - try: - subnets = _describe_subnets(client, Filters=[ - {'Name': 'subnet-id', 'Values': params}]) - all_found.extend(subnets.get('Subnets', [])) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - if len(params) != len(all_found): - try: - subnets = _describe_subnets(client, Filters=[ - {'Name': 'tag:Name', 'Values': params}]) - all_found.extend(subnets.get('Subnets', [])) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return list(set(s['SubnetId'] for s in all_found if s.get('SubnetId'))) - - -def main(): - argument_spec = dict( - vpc_id=dict(), - name=dict(), - nacl_id=dict(), - subnets=dict(required=False, type='list', default=list()), - tags=dict(required=False, type='dict'), - ingress=dict(required=False, type='list', default=list()), - egress=dict(required=False, type='list', default=list()), - state=dict(default='present', choices=['present', 'absent']), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=[['name', 'nacl_id']], - required_if=[['state', 'present', ['vpc_id']]]) - - state = module.params.get('state').lower() - - client = module.client('ec2') - - invocations = { - "present": setup_network_acl, - "absent": remove_network_acl - } - (changed, results) = invocations[state](client, module) - module.exit_json(changed=changed, nacl_id=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl_info.py deleted file mode 100644 index c643d23413..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_nacl_info.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/python -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_nacl_info -short_description: Gather information about Network ACLs in an AWS VPC -description: - - Gather information about Network ACLs in an AWS VPC - - This module was called C(ec2_vpc_nacl_facts) before Ansible 2.9. The usage did not change. -version_added: "2.2" -author: "Brad Davidson (@brandond)" -requirements: [ boto3 ] -options: - nacl_ids: - description: - - A list of Network ACL IDs to retrieve information about. - required: false - default: [] - aliases: [nacl_id] - type: list - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkAcls.html) for possible filters. Filter - names and values are case sensitive. - required: false - default: {} - type: dict -notes: - - By default, the module will return all Network ACLs. - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all Network ACLs: -- name: Get All NACLs - register: all_nacls - ec2_vpc_nacl_info: - region: us-west-2 - -# Retrieve default Network ACLs: -- name: Get Default NACLs - register: default_nacls - ec2_vpc_nacl_info: - region: us-west-2 - filters: - 'default': 'true' -''' - -RETURN = ''' -nacls: - description: Returns an array of complex objects as described below. - returned: success - type: complex - contains: - nacl_id: - description: The ID of the Network Access Control List. - returned: always - type: str - vpc_id: - description: The ID of the VPC that the NACL is attached to. - returned: always - type: str - is_default: - description: True if the NACL is the default for its VPC. - returned: always - type: bool - tags: - description: A dict of tags associated with the NACL. - returned: always - type: dict - subnets: - description: A list of subnet IDs that are associated with the NACL. - returned: always - type: list - elements: str - ingress: - description: - - A list of NACL ingress rules with the following format. - - "C([rule no, protocol, allow/deny, v4 or v6 cidr, icmp_type, icmp_code, port from, port to])" - returned: always - type: list - elements: list - sample: [[100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22]] - egress: - description: - - A list of NACL egress rules with the following format. - - "C([rule no, protocol, allow/deny, v4 or v6 cidr, icmp_type, icmp_code, port from, port to])" - returned: always - type: list - elements: list - sample: [[100, 'all', 'allow', '0.0.0.0/0', null, null, null, null]] -''' - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils._text import to_native -from ansible.module_utils.ec2 import (AWSRetry, ansible_dict_to_boto3_filter_list, - camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict) - - -# VPC-supported IANA protocol numbers -# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml -PROTOCOL_NAMES = {'-1': 'all', '1': 'icmp', '6': 'tcp', '17': 'udp'} - - -def list_ec2_vpc_nacls(connection, module): - - nacl_ids = module.params.get("nacl_ids") - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - if nacl_ids is None: - nacl_ids = [] - - try: - nacls = connection.describe_network_acls(aws_retry=True, NetworkAclIds=nacl_ids, Filters=filters) - except ClientError as e: - if e.response['Error']['Code'] == 'InvalidNetworkAclID.NotFound': - module.fail_json(msg='Unable to describe ACL. NetworkAcl does not exist') - module.fail_json_aws(e, msg="Unable to describe network ACLs {0}".format(nacl_ids)) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to describe network ACLs {0}".format(nacl_ids)) - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_nacls = [] - for nacl in nacls['NetworkAcls']: - snaked_nacls.append(camel_dict_to_snake_dict(nacl)) - - # Turn the boto3 result in to ansible friendly tag dictionary - for nacl in snaked_nacls: - if 'tags' in nacl: - nacl['tags'] = boto3_tag_list_to_ansible_dict(nacl['tags'], 'key', 'value') - if 'entries' in nacl: - nacl['egress'] = [nacl_entry_to_list(entry) for entry in nacl['entries'] - if entry['rule_number'] < 32767 and entry['egress']] - nacl['ingress'] = [nacl_entry_to_list(entry) for entry in nacl['entries'] - if entry['rule_number'] < 32767 and not entry['egress']] - del nacl['entries'] - if 'associations' in nacl: - nacl['subnets'] = [a['subnet_id'] for a in nacl['associations']] - del nacl['associations'] - if 'network_acl_id' in nacl: - nacl['nacl_id'] = nacl['network_acl_id'] - del nacl['network_acl_id'] - - module.exit_json(nacls=snaked_nacls) - - -def nacl_entry_to_list(entry): - - # entry list format - # [ rule_num, protocol name or number, allow or deny, ipv4/6 cidr, icmp type, icmp code, port from, port to] - elist = [] - - elist.append(entry['rule_number']) - - if entry.get('protocol') in PROTOCOL_NAMES: - elist.append(PROTOCOL_NAMES[entry['protocol']]) - else: - elist.append(entry.get('protocol')) - - elist.append(entry['rule_action']) - - if entry.get('cidr_block'): - elist.append(entry['cidr_block']) - elif entry.get('ipv6_cidr_block'): - elist.append(entry['ipv6_cidr_block']) - else: - elist.append(None) - - elist = elist + [None, None, None, None] - - if entry['protocol'] in ('1', '58'): - elist[4] = entry.get('icmp_type_code', {}).get('type') - elist[5] = entry.get('icmp_type_code', {}).get('code') - - if entry['protocol'] not in ('1', '6', '17', '58'): - elist[6] = 0 - elist[7] = 65535 - elif 'port_range' in entry: - elist[6] = entry['port_range']['from'] - elist[7] = entry['port_range']['to'] - - return elist - - -def main(): - - argument_spec = dict( - nacl_ids=dict(default=[], type='list', aliases=['nacl_id']), - filters=dict(default={}, type='dict')) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vpc_nacl_facts': - module.deprecate("The 'ec2_vpc_nacl_facts' module has been renamed to 'ec2_vpc_nacl_info'", version='2.13') - - connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - - list_ec2_vpc_nacls(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway.py deleted file mode 100644 index 7598b23266..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway.py +++ /dev/null @@ -1,1020 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_nat_gateway -short_description: Manage AWS VPC NAT Gateways. -description: - - Ensure the state of AWS VPC NAT Gateways based on their id, allocation and subnet ids. -version_added: "2.2" -requirements: [boto3, botocore] -options: - state: - description: - - Ensure NAT Gateway is present or absent. - default: "present" - choices: ["present", "absent"] - type: str - nat_gateway_id: - description: - - The id AWS dynamically allocates to the NAT Gateway on creation. - This is required when the absent option is present. - type: str - subnet_id: - description: - - The id of the subnet to create the NAT Gateway in. This is required - with the present option. - type: str - allocation_id: - description: - - The id of the elastic IP allocation. If this is not passed and the - eip_address is not passed. An EIP is generated for this NAT Gateway. - type: str - eip_address: - description: - - The elastic IP address of the EIP you want attached to this NAT Gateway. - If this is not passed and the allocation_id is not passed, - an EIP is generated for this NAT Gateway. - type: str - if_exist_do_not_create: - description: - - if a NAT Gateway exists already in the subnet_id, then do not create a new one. - required: false - default: false - type: bool - release_eip: - description: - - Deallocate the EIP from the VPC. - - Option is only valid with the absent state. - - You should use this with the wait option. Since you can not release an address while a delete operation is happening. - default: false - type: bool - wait: - description: - - Wait for operation to complete before returning. - default: false - type: bool - wait_timeout: - description: - - How many seconds to wait for an operation to complete before timing out. - default: 320 - type: int - client_token: - description: - - Optional unique token to be used during create to ensure idempotency. - When specifying this option, ensure you specify the eip_address parameter - as well otherwise any subsequent runs will fail. - type: str -author: - - Allen Sanabria (@linuxdynasty) - - Jon Hadfield (@jonhadfield) - - Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Create new nat gateway with client token. - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - eip_address: 52.1.1.1 - region: ap-southeast-2 - client_token: abcd-12345678 - register: new_nat_gateway - -- name: Create new nat gateway using an allocation-id. - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - allocation_id: eipalloc-12345678 - region: ap-southeast-2 - register: new_nat_gateway - -- name: Create new nat gateway, using an EIP address and wait for available status. - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - eip_address: 52.1.1.1 - wait: true - region: ap-southeast-2 - register: new_nat_gateway - -- name: Create new nat gateway and allocate new EIP. - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - wait: true - region: ap-southeast-2 - register: new_nat_gateway - -- name: Create new nat gateway and allocate new EIP if a nat gateway does not yet exist in the subnet. - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - wait: true - region: ap-southeast-2 - if_exist_do_not_create: true - register: new_nat_gateway - -- name: Delete nat gateway using discovered nat gateways from facts module. - ec2_vpc_nat_gateway: - state: absent - region: ap-southeast-2 - wait: true - nat_gateway_id: "{{ item.NatGatewayId }}" - release_eip: true - register: delete_nat_gateway_result - loop: "{{ gateways_to_remove.result }}" - -- name: Delete nat gateway and wait for deleted status. - ec2_vpc_nat_gateway: - state: absent - nat_gateway_id: nat-12345678 - wait: true - wait_timeout: 500 - region: ap-southeast-2 - -- name: Delete nat gateway and release EIP. - ec2_vpc_nat_gateway: - state: absent - nat_gateway_id: nat-12345678 - release_eip: true - wait: yes - wait_timeout: 300 - region: ap-southeast-2 -''' - -RETURN = ''' -create_time: - description: The ISO 8601 date time format in UTC. - returned: In all cases. - type: str - sample: "2016-03-05T05:19:20.282000+00:00'" -nat_gateway_id: - description: id of the VPC NAT Gateway - returned: In all cases. - type: str - sample: "nat-0d1e3a878585988f8" -subnet_id: - description: id of the Subnet - returned: In all cases. - type: str - sample: "subnet-12345" -state: - description: The current state of the NAT Gateway. - returned: In all cases. - type: str - sample: "available" -vpc_id: - description: id of the VPC. - returned: In all cases. - type: str - sample: "vpc-12345" -nat_gateway_addresses: - description: List of dictionaries containing the public_ip, network_interface_id, private_ip, and allocation_id. - returned: In all cases. - type: str - sample: [ - { - 'public_ip': '52.52.52.52', - 'network_interface_id': 'eni-12345', - 'private_ip': '10.0.0.100', - 'allocation_id': 'eipalloc-12345' - } - ] -''' - -import datetime -import random -import time - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, - camel_dict_to_snake_dict, HAS_BOTO3) - - -DRY_RUN_GATEWAYS = [ - { - "nat_gateway_id": "nat-123456789", - "subnet_id": "subnet-123456789", - "nat_gateway_addresses": [ - { - "public_ip": "55.55.55.55", - "network_interface_id": "eni-1234567", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-1234567" - } - ], - "state": "available", - "create_time": "2016-03-05T05:19:20.282000+00:00", - "vpc_id": "vpc-12345678" - } -] - -DRY_RUN_ALLOCATION_UNCONVERTED = { - 'Addresses': [ - { - 'PublicIp': '55.55.55.55', - 'Domain': 'vpc', - 'AllocationId': 'eipalloc-1234567' - } - ] -} - -DRY_RUN_MSGS = 'DryRun Mode:' - - -def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, - states=None, check_mode=False): - """Retrieve a list of NAT Gateways - Args: - client (botocore.client.EC2): Boto3 client - - Kwargs: - subnet_id (str): The subnet_id the nat resides in. - nat_gateway_id (str): The Amazon nat id. - states (list): States available (pending, failed, available, deleting, and deleted) - default=None - - Basic Usage: - >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-12345678' - >>> get_nat_gateways(client, subnet_id) - [ - true, - "", - { - "nat_gateway_id": "nat-123456789", - "subnet_id": "subnet-123456789", - "nat_gateway_addresses": [ - { - "public_ip": "55.55.55.55", - "network_interface_id": "eni-1234567", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-1234567" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-12345678" - } - - Returns: - Tuple (bool, str, list) - """ - params = dict() - err_msg = "" - gateways_retrieved = False - existing_gateways = list() - if not states: - states = ['available', 'pending'] - if nat_gateway_id: - params['NatGatewayIds'] = [nat_gateway_id] - else: - params['Filter'] = [ - { - 'Name': 'subnet-id', - 'Values': [subnet_id] - }, - { - 'Name': 'state', - 'Values': states - } - ] - - try: - if not check_mode: - gateways = client.describe_nat_gateways(**params)['NatGateways'] - if gateways: - for gw in gateways: - existing_gateways.append(camel_dict_to_snake_dict(gw)) - gateways_retrieved = True - else: - gateways_retrieved = True - if nat_gateway_id: - if DRY_RUN_GATEWAYS[0]['nat_gateway_id'] == nat_gateway_id: - existing_gateways = DRY_RUN_GATEWAYS - elif subnet_id: - if DRY_RUN_GATEWAYS[0]['subnet_id'] == subnet_id: - existing_gateways = DRY_RUN_GATEWAYS - err_msg = '{0} Retrieving gateways'.format(DRY_RUN_MSGS) - - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - return gateways_retrieved, err_msg, existing_gateways - - -def wait_for_status(client, wait_timeout, nat_gateway_id, status, - check_mode=False): - """Wait for the NAT Gateway to reach a status - Args: - client (botocore.client.EC2): Boto3 client - wait_timeout (int): Number of seconds to wait, until this timeout is reached. - nat_gateway_id (str): The Amazon nat id. - status (str): The status to wait for. - examples. status=available, status=deleted - - Basic Usage: - >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-12345678' - >>> allocation_id = 'eipalloc-12345678' - >>> wait_for_status(client, subnet_id, allocation_id) - [ - true, - "", - { - "nat_gateway_id": "nat-123456789", - "subnet_id": "subnet-1234567", - "nat_gateway_addresses": [ - { - "public_ip": "55.55.55.55", - "network_interface_id": "eni-1234567", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-12345678" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-12345677" - } - ] - - Returns: - Tuple (bool, str, dict) - """ - polling_increment_secs = 5 - wait_timeout = time.time() + wait_timeout - status_achieved = False - nat_gateway = dict() - states = ['pending', 'failed', 'available', 'deleting', 'deleted'] - err_msg = "" - - while wait_timeout > time.time(): - try: - gws_retrieved, err_msg, nat_gateways = ( - get_nat_gateways( - client, nat_gateway_id=nat_gateway_id, - states=states, check_mode=check_mode - ) - ) - if gws_retrieved and nat_gateways: - nat_gateway = nat_gateways[0] - if check_mode: - nat_gateway['state'] = status - - if nat_gateway.get('state') == status: - status_achieved = True - break - - elif nat_gateway.get('state') == 'failed': - err_msg = nat_gateway.get('failure_message') - break - - elif nat_gateway.get('state') == 'pending': - if 'failure_message' in nat_gateway: - err_msg = nat_gateway.get('failure_message') - status_achieved = False - break - - else: - time.sleep(polling_increment_secs) - - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - if not status_achieved: - err_msg = "Wait time out reached, while waiting for results" - - return status_achieved, err_msg, nat_gateway - - -def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, - check_mode=False): - """Retrieve all NAT Gateways for a subnet. - Args: - subnet_id (str): The subnet_id the nat resides in. - - Kwargs: - allocation_id (str): The EIP Amazon identifier. - default = None - - Basic Usage: - >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-1234567' - >>> allocation_id = 'eipalloc-1234567' - >>> gateway_in_subnet_exists(client, subnet_id, allocation_id) - ( - [ - { - "nat_gateway_id": "nat-123456789", - "subnet_id": "subnet-123456789", - "nat_gateway_addresses": [ - { - "public_ip": "55.55.55.55", - "network_interface_id": "eni-1234567", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-1234567" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-1234567" - } - ], - False - ) - - Returns: - Tuple (list, bool) - """ - allocation_id_exists = False - gateways = [] - states = ['available', 'pending'] - gws_retrieved, err_msg, gws = ( - get_nat_gateways( - client, subnet_id, states=states, check_mode=check_mode - ) - ) - if not gws_retrieved: - return gateways, allocation_id_exists - for gw in gws: - for address in gw['nat_gateway_addresses']: - if allocation_id: - if address.get('allocation_id') == allocation_id: - allocation_id_exists = True - gateways.append(gw) - else: - gateways.append(gw) - - return gateways, allocation_id_exists - - -def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): - """Release an EIP from your EIP Pool - Args: - client (botocore.client.EC2): Boto3 client - eip_address (str): The Elastic IP Address of the EIP. - - Kwargs: - check_mode (bool): if set to true, do not run anything and - falsify the results. - - Basic Usage: - >>> client = boto3.client('ec2') - >>> eip_address = '52.87.29.36' - >>> get_eip_allocation_id_by_address(client, eip_address) - 'eipalloc-36014da3' - - Returns: - Tuple (str, str) - """ - params = { - 'PublicIps': [eip_address], - } - allocation_id = None - err_msg = "" - try: - if not check_mode: - allocations = client.describe_addresses(**params)['Addresses'] - if len(allocations) == 1: - allocation = allocations[0] - else: - allocation = None - else: - dry_run_eip = ( - DRY_RUN_ALLOCATION_UNCONVERTED['Addresses'][0]['PublicIp'] - ) - if dry_run_eip == eip_address: - allocation = DRY_RUN_ALLOCATION_UNCONVERTED['Addresses'][0] - else: - allocation = None - if allocation: - if allocation.get('Domain') != 'vpc': - err_msg = ( - "EIP {0} is a non-VPC EIP, please allocate a VPC scoped EIP" - .format(eip_address) - ) - else: - allocation_id = allocation.get('AllocationId') - else: - err_msg = ( - "EIP {0} does not exist".format(eip_address) - ) - - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - return allocation_id, err_msg - - -def allocate_eip_address(client, check_mode=False): - """Release an EIP from your EIP Pool - Args: - client (botocore.client.EC2): Boto3 client - - Kwargs: - check_mode (bool): if set to true, do not run anything and - falsify the results. - - Basic Usage: - >>> client = boto3.client('ec2') - >>> allocate_eip_address(client) - True - - Returns: - Tuple (bool, str) - """ - ip_allocated = False - new_eip = None - err_msg = '' - params = { - 'Domain': 'vpc', - } - try: - if check_mode: - ip_allocated = True - random_numbers = ( - ''.join(str(x) for x in random.sample(range(0, 9), 7)) - ) - new_eip = 'eipalloc-{0}'.format(random_numbers) - else: - new_eip = client.allocate_address(**params)['AllocationId'] - ip_allocated = True - err_msg = 'eipalloc id {0} created'.format(new_eip) - - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - return ip_allocated, err_msg, new_eip - - -def release_address(client, allocation_id, check_mode=False): - """Release an EIP from your EIP Pool - Args: - client (botocore.client.EC2): Boto3 client - allocation_id (str): The eip Amazon identifier. - - Kwargs: - check_mode (bool): if set to true, do not run anything and - falsify the results. - - Basic Usage: - >>> client = boto3.client('ec2') - >>> allocation_id = "eipalloc-123456" - >>> release_address(client, allocation_id) - True - - Returns: - Boolean, string - """ - err_msg = '' - if check_mode: - return True, '' - - ip_released = False - try: - client.describe_addresses(AllocationIds=[allocation_id]) - except botocore.exceptions.ClientError as e: - # IP address likely already released - # Happens with gateway in 'deleted' state that - # still lists associations - return True, str(e) - try: - client.release_address(AllocationId=allocation_id) - ip_released = True - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - return ip_released, err_msg - - -def create(client, subnet_id, allocation_id, client_token=None, - wait=False, wait_timeout=0, if_exist_do_not_create=False, - check_mode=False): - """Create an Amazon NAT Gateway. - Args: - client (botocore.client.EC2): Boto3 client - subnet_id (str): The subnet_id the nat resides in. - allocation_id (str): The eip Amazon identifier. - - Kwargs: - if_exist_do_not_create (bool): if a nat gateway already exists in this - subnet, than do not create another one. - default = False - wait (bool): Wait for the nat to be in the deleted state before returning. - default = False - wait_timeout (int): Number of seconds to wait, until this timeout is reached. - default = 0 - client_token (str): - default = None - - Basic Usage: - >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-1234567' - >>> allocation_id = 'eipalloc-1234567' - >>> create(client, subnet_id, allocation_id, if_exist_do_not_create=True, wait=True, wait_timeout=500) - [ - true, - "", - { - "nat_gateway_id": "nat-123456789", - "subnet_id": "subnet-1234567", - "nat_gateway_addresses": [ - { - "public_ip": "55.55.55.55", - "network_interface_id": "eni-1234567", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-1234567" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-1234567" - } - ] - - Returns: - Tuple (bool, str, list) - """ - params = { - 'SubnetId': subnet_id, - 'AllocationId': allocation_id - } - request_time = datetime.datetime.utcnow() - changed = False - success = False - token_provided = False - err_msg = "" - - if client_token: - token_provided = True - params['ClientToken'] = client_token - - try: - if not check_mode: - result = camel_dict_to_snake_dict(client.create_nat_gateway(**params)["NatGateway"]) - else: - result = DRY_RUN_GATEWAYS[0] - result['create_time'] = datetime.datetime.utcnow() - result['nat_gateway_addresses'][0]['allocation_id'] = allocation_id - result['subnet_id'] = subnet_id - - success = True - changed = True - create_time = result['create_time'].replace(tzinfo=None) - if token_provided and (request_time > create_time): - changed = False - elif wait: - success, err_msg, result = ( - wait_for_status( - client, wait_timeout, result['nat_gateway_id'], 'available', - check_mode=check_mode - ) - ) - if success: - err_msg = ( - 'NAT gateway {0} created'.format(result['nat_gateway_id']) - ) - - except botocore.exceptions.ClientError as e: - if "IdempotentParameterMismatch" in e.message: - err_msg = ( - 'NAT Gateway does not support update and token has already been provided: ' + str(e) - ) - else: - err_msg = str(e) - success = False - changed = False - result = None - - return success, changed, err_msg, result - - -def pre_create(client, subnet_id, allocation_id=None, eip_address=None, - if_exist_do_not_create=False, wait=False, wait_timeout=0, - client_token=None, check_mode=False): - """Create an Amazon NAT Gateway. - Args: - client (botocore.client.EC2): Boto3 client - subnet_id (str): The subnet_id the nat resides in. - - Kwargs: - allocation_id (str): The EIP Amazon identifier. - default = None - eip_address (str): The Elastic IP Address of the EIP. - default = None - if_exist_do_not_create (bool): if a nat gateway already exists in this - subnet, than do not create another one. - default = False - wait (bool): Wait for the nat to be in the deleted state before returning. - default = False - wait_timeout (int): Number of seconds to wait, until this timeout is reached. - default = 0 - client_token (str): - default = None - - Basic Usage: - >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' - >>> pre_create(client, subnet_id, allocation_id, if_exist_do_not_create=True, wait=True, wait_timeout=500) - [ - true, - "", - { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", - "nat_gateway_addresses": [ - { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" - } - ] - - Returns: - Tuple (bool, bool, str, list) - """ - success = False - changed = False - err_msg = "" - results = list() - - if not allocation_id and not eip_address: - existing_gateways, allocation_id_exists = ( - gateway_in_subnet_exists(client, subnet_id, check_mode=check_mode) - ) - - if len(existing_gateways) > 0 and if_exist_do_not_create: - success = True - changed = False - results = existing_gateways[0] - err_msg = ( - 'NAT Gateway {0} already exists in subnet_id {1}' - .format( - existing_gateways[0]['nat_gateway_id'], subnet_id - ) - ) - return success, changed, err_msg, results - else: - success, err_msg, allocation_id = ( - allocate_eip_address(client, check_mode=check_mode) - ) - if not success: - return success, 'False', err_msg, dict() - - elif eip_address or allocation_id: - if eip_address and not allocation_id: - allocation_id, err_msg = ( - get_eip_allocation_id_by_address( - client, eip_address, check_mode=check_mode - ) - ) - if not allocation_id: - success = False - changed = False - return success, changed, err_msg, dict() - - existing_gateways, allocation_id_exists = ( - gateway_in_subnet_exists( - client, subnet_id, allocation_id, check_mode=check_mode - ) - ) - if len(existing_gateways) > 0 and (allocation_id_exists or if_exist_do_not_create): - success = True - changed = False - results = existing_gateways[0] - err_msg = ( - 'NAT Gateway {0} already exists in subnet_id {1}' - .format( - existing_gateways[0]['nat_gateway_id'], subnet_id - ) - ) - return success, changed, err_msg, results - - success, changed, err_msg, results = create( - client, subnet_id, allocation_id, client_token, - wait, wait_timeout, if_exist_do_not_create, check_mode=check_mode - ) - - return success, changed, err_msg, results - - -def remove(client, nat_gateway_id, wait=False, wait_timeout=0, - release_eip=False, check_mode=False): - """Delete an Amazon NAT Gateway. - Args: - client (botocore.client.EC2): Boto3 client - nat_gateway_id (str): The Amazon nat id. - - Kwargs: - wait (bool): Wait for the nat to be in the deleted state before returning. - wait_timeout (int): Number of seconds to wait, until this timeout is reached. - release_eip (bool): Once the nat has been deleted, you can deallocate the eip from the vpc. - - Basic Usage: - >>> client = boto3.client('ec2') - >>> nat_gw_id = 'nat-03835afb6e31df79b' - >>> remove(client, nat_gw_id, wait=True, wait_timeout=500, release_eip=True) - [ - true, - "", - { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", - "nat_gateway_addresses": [ - { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", - "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" - } - ], - "state": "deleted", - "create_time": "2016-03-05T00:33:21.209000+00:00", - "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" - } - ] - - Returns: - Tuple (bool, str, list) - """ - params = { - 'NatGatewayId': nat_gateway_id - } - success = False - changed = False - err_msg = "" - results = list() - states = ['pending', 'available'] - try: - exist, err_msg, gw = ( - get_nat_gateways( - client, nat_gateway_id=nat_gateway_id, - states=states, check_mode=check_mode - ) - ) - if exist and len(gw) == 1: - results = gw[0] - if not check_mode: - client.delete_nat_gateway(**params) - - allocation_id = ( - results['nat_gateway_addresses'][0]['allocation_id'] - ) - changed = True - success = True - err_msg = ( - 'NAT gateway {0} is in a deleting state. Delete was successful' - .format(nat_gateway_id) - ) - - if wait: - status_achieved, err_msg, results = ( - wait_for_status( - client, wait_timeout, nat_gateway_id, 'deleted', - check_mode=check_mode - ) - ) - if status_achieved: - err_msg = ( - 'NAT gateway {0} was deleted successfully' - .format(nat_gateway_id) - ) - - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - if release_eip: - eip_released, eip_err = ( - release_address(client, allocation_id, check_mode) - ) - if not eip_released: - err_msg = ( - "{0}: Failed to release EIP {1}: {2}" - .format(err_msg, allocation_id, eip_err) - ) - success = False - - return success, changed, err_msg, results - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - subnet_id=dict(type='str'), - eip_address=dict(type='str'), - allocation_id=dict(type='str'), - if_exist_do_not_create=dict(type='bool', default=False), - state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=320, required=False), - release_eip=dict(type='bool', default=False), - nat_gateway_id=dict(type='str'), - client_token=dict(type='str'), - ) - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ - ['allocation_id', 'eip_address'] - ], - required_if=[['state', 'absent', ['nat_gateway_id']], - ['state', 'present', ['subnet_id']]] - ) - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore/boto3 is required.') - - state = module.params.get('state').lower() - check_mode = module.check_mode - subnet_id = module.params.get('subnet_id') - allocation_id = module.params.get('allocation_id') - eip_address = module.params.get('eip_address') - nat_gateway_id = module.params.get('nat_gateway_id') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - release_eip = module.params.get('release_eip') - client_token = module.params.get('client_token') - if_exist_do_not_create = module.params.get('if_exist_do_not_create') - - try: - region, ec2_url, aws_connect_kwargs = ( - get_aws_connection_info(module, boto3=True) - ) - client = ( - boto3_conn( - module, conn_type='client', resource='ec2', - region=region, endpoint=ec2_url, **aws_connect_kwargs - ) - ) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) - - changed = False - err_msg = '' - - if state == 'present': - success, changed, err_msg, results = ( - pre_create( - client, subnet_id, allocation_id, eip_address, - if_exist_do_not_create, wait, wait_timeout, - client_token, check_mode=check_mode - ) - ) - else: - success, changed, err_msg, results = ( - remove( - client, nat_gateway_id, wait, wait_timeout, release_eip, - check_mode=check_mode - ) - ) - - if not success: - module.fail_json( - msg=err_msg, success=success, changed=changed - ) - else: - module.exit_json( - msg=err_msg, success=success, changed=changed, **results - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway_info.py deleted file mode 100644 index 6ecb27b588..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_nat_gateway_info.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_vpc_nat_gateway_info -short_description: Retrieves AWS VPC Managed Nat Gateway details using AWS methods. -description: - - Gets various details related to AWS VPC Managed Nat Gateways - - This module was called C(ec2_vpc_nat_gateway_facts) before Ansible 2.9. The usage did not change. -version_added: "2.3" -requirements: [ boto3 ] -options: - nat_gateway_ids: - description: - - List of specific nat gateway IDs to fetch details for. - type: list - elements: str - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNatGateways.html) - for possible filters. - type: dict -author: Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple example of listing all nat gateways -- name: List all managed nat gateways in ap-southeast-2 - ec2_vpc_nat_gateway_info: - region: ap-southeast-2 - register: all_ngws - -- name: Debugging the result - debug: - msg: "{{ all_ngws.result }}" - -- name: Get details on specific nat gateways - ec2_vpc_nat_gateway_info: - nat_gateway_ids: - - nat-1234567891234567 - - nat-7654321987654321 - region: ap-southeast-2 - register: specific_ngws - -- name: Get all nat gateways with specific filters - ec2_vpc_nat_gateway_info: - region: ap-southeast-2 - filters: - state: ['pending'] - register: pending_ngws - -- name: Get nat gateways with specific filter - ec2_vpc_nat_gateway_info: - region: ap-southeast-2 - filters: - subnet-id: subnet-12345678 - state: ['available'] - register: existing_nat_gateways -''' - -RETURN = ''' -result: - description: The result of the describe, converted to ansible snake case style. - See http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_nat_gateways for the response. - returned: success - type: list -''' - -import json - -try: - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, - camel_dict_to_snake_dict, ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict, HAS_BOTO3) - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def get_nat_gateways(client, module, nat_gateway_id=None): - params = dict() - nat_gateways = list() - - params['Filter'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - params['NatGatewayIds'] = module.params.get('nat_gateway_ids') - - try: - result = json.loads(json.dumps(client.describe_nat_gateways(**params), default=date_handler)) - except Exception as e: - module.fail_json(msg=str(e.message)) - - for gateway in result['NatGateways']: - # Turn the boto3 result into ansible_friendly_snaked_names - converted_gateway = camel_dict_to_snake_dict(gateway) - if 'tags' in converted_gateway: - # Turn the boto3 result into ansible friendly tag dictionary - converted_gateway['tags'] = boto3_tag_list_to_ansible_dict(converted_gateway['tags']) - - nat_gateways.append(converted_gateway) - - return nat_gateways - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(default={}, type='dict'), - nat_gateway_ids=dict(default=[], type='list'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_vpc_nat_gateway_facts': - module.deprecate("The 'ec2_vpc_nat_gateway_facts' module has been renamed to 'ec2_vpc_nat_gateway_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore/boto3 is required.') - - try: - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg=str(e)) - - results = get_nat_gateways(connection, module) - - module.exit_json(result=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_peer.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_peer.py deleted file mode 100644 index fff0a97528..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_peer.py +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_vpc_peer -short_description: create, delete, accept, and reject VPC peering connections between two VPCs. -description: - - Read the AWS documentation for VPC Peering Connections - U(https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-peering.html). -version_added: "2.2" -options: - vpc_id: - description: - - VPC id of the requesting VPC. - required: false - type: str - peering_id: - description: - - Peering connection id. - required: false - type: str - peer_region: - description: - - Region of the accepting VPC. - required: false - version_added: '2.5' - type: str - peer_vpc_id: - description: - - VPC id of the accepting VPC. - required: false - type: str - peer_owner_id: - description: - - The AWS account number for cross account peering. - required: false - type: str - tags: - description: - - Dictionary of tags to look for and apply when creating a Peering Connection. - required: false - type: dict - state: - description: - - Create, delete, accept, reject a peering connection. - required: false - default: present - choices: ['present', 'absent', 'accept', 'reject'] - type: str -author: Mike Mochan (@mmochan) -extends_documentation_fragment: - - aws - - ec2 -requirements: [ botocore, boto3, json ] -''' - -EXAMPLES = ''' -# Complete example to create and accept a local peering connection. -- name: Create local account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Accept local VPC peering request - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - state: accept - register: action_peer - -# Complete example to delete a local peering connection. -- name: Create local account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: delete a local VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - state: absent - register: vpc_peer - - # Complete example to create and accept a cross account peering connection. -- name: Create cross account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789102 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Accept peering connection from remote account - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: accept - register: vpc_peer - -# Complete example to create and accept an intra-region peering connection. -- name: Create intra-region VPC peering Connection - ec2_vpc_peer: - region: us-east-1 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - peer_region: us-west-2 - state: present - tags: - Name: Peering connection for us-east-1 VPC to us-west-2 VPC - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Accept peering connection from peer region - ec2_vpc_peer: - region: us-west-2 - peering_id: "{{ vpc_peer.peering_id }}" - state: accept - register: vpc_peer - -# Complete example to create and reject a local peering connection. -- name: Create local account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Reject a local VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - state: reject - -# Complete example to create and accept a cross account peering connection. -- name: Create cross account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789102 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Accept a cross account VPC peering connection request - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: accept - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - -# Complete example to create and reject a cross account peering connection. -- name: Create cross account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789102 - state: present - tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix - register: vpc_peer - -- name: Reject a cross account VPC peering Connection - ec2_vpc_peer: - region: ap-southeast-2 - peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: reject - -''' -RETURN = ''' -task: - description: The result of the create, accept, reject or delete action. - returned: success - type: dict -''' - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -import distutils.version -import traceback - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info, HAS_BOTO3 -from ansible.module_utils.aws.core import is_boto3_error_code - - -def tags_changed(pcx_id, client, module): - changed = False - tags = dict() - if module.params.get('tags'): - tags = module.params.get('tags') - pcx = find_pcx_by_id(pcx_id, client, module) - if pcx['VpcPeeringConnections']: - pcx_values = [t.values() for t in pcx['VpcPeeringConnections'][0]['Tags']] - pcx_tags = [item for sublist in pcx_values for item in sublist] - tag_values = [[key, str(value)] for key, value in tags.items()] - tags = [item for sublist in tag_values for item in sublist] - if sorted(pcx_tags) == sorted(tags): - changed = False - elif tags: - delete_tags(pcx_id, client, module) - create_tags(pcx_id, client, module) - changed = True - return changed - - -def describe_peering_connections(params, client): - result = client.describe_vpc_peering_connections( - Filters=[ - {'Name': 'requester-vpc-info.vpc-id', 'Values': [params['VpcId']]}, - {'Name': 'accepter-vpc-info.vpc-id', 'Values': [params['PeerVpcId']]} - ] - ) - if result['VpcPeeringConnections'] == []: - result = client.describe_vpc_peering_connections( - Filters=[ - {'Name': 'requester-vpc-info.vpc-id', 'Values': [params['PeerVpcId']]}, - {'Name': 'accepter-vpc-info.vpc-id', 'Values': [params['VpcId']]} - ] - ) - return result - - -def is_active(peering_conn): - return peering_conn['Status']['Code'] == 'active' - - -def is_pending(peering_conn): - return peering_conn['Status']['Code'] == 'pending-acceptance' - - -def create_peer_connection(client, module): - changed = False - params = dict() - params['VpcId'] = module.params.get('vpc_id') - params['PeerVpcId'] = module.params.get('peer_vpc_id') - if module.params.get('peer_region'): - if distutils.version.StrictVersion(botocore.__version__) < distutils.version.StrictVersion('1.8.6'): - module.fail_json(msg="specifying peer_region parameter requires botocore >= 1.8.6") - params['PeerRegion'] = module.params.get('peer_region') - if module.params.get('peer_owner_id'): - params['PeerOwnerId'] = str(module.params.get('peer_owner_id')) - peering_conns = describe_peering_connections(params, client) - for peering_conn in peering_conns['VpcPeeringConnections']: - pcx_id = peering_conn['VpcPeeringConnectionId'] - if tags_changed(pcx_id, client, module): - changed = True - if is_active(peering_conn): - return (changed, peering_conn['VpcPeeringConnectionId']) - if is_pending(peering_conn): - return (changed, peering_conn['VpcPeeringConnectionId']) - try: - peering_conn = client.create_vpc_peering_connection(**params) - pcx_id = peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId'] - if module.params.get('tags'): - create_tags(pcx_id, client, module) - changed = True - return (changed, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - -def remove_peer_connection(client, module): - pcx_id = module.params.get('peering_id') - if not pcx_id: - params = dict() - params['VpcId'] = module.params.get('vpc_id') - params['PeerVpcId'] = module.params.get('peer_vpc_id') - params['PeerRegion'] = module.params.get('peer_region') - if module.params.get('peer_owner_id'): - params['PeerOwnerId'] = str(module.params.get('peer_owner_id')) - peering_conns = describe_peering_connections(params, client) - if not peering_conns: - module.exit_json(changed=False) - else: - pcx_id = peering_conns['VpcPeeringConnections'][0]['VpcPeeringConnectionId'] - - try: - params = dict() - params['VpcPeeringConnectionId'] = pcx_id - client.delete_vpc_peering_connection(**params) - module.exit_json(changed=True) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - -def peer_status(client, module): - params = dict() - params['VpcPeeringConnectionIds'] = [module.params.get('peering_id')] - try: - vpc_peering_connection = client.describe_vpc_peering_connections(**params) - return vpc_peering_connection['VpcPeeringConnections'][0]['Status']['Code'] - except is_boto3_error_code('InvalidVpcPeeringConnectionId.Malformed') as e: # pylint: disable=duplicate-except - module.fail_json(msg='Malformed connection ID: {0}'.format(e), traceback=traceback.format_exc()) - except botocore.exceptions.ClientError as e: # pylint: disable=duplicate-except - module.fail_json(msg='Error while describing peering connection by peering_id: {0}'.format(e), traceback=traceback.format_exc()) - - -def accept_reject(state, client, module): - changed = False - params = dict() - params['VpcPeeringConnectionId'] = module.params.get('peering_id') - if peer_status(client, module) != 'active': - try: - if state == 'accept': - client.accept_vpc_peering_connection(**params) - else: - client.reject_vpc_peering_connection(**params) - if module.params.get('tags'): - create_tags(params['VpcPeeringConnectionId'], client, module) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - if tags_changed(params['VpcPeeringConnectionId'], client, module): - changed = True - return changed, params['VpcPeeringConnectionId'] - - -def load_tags(module): - tags = [] - if module.params.get('tags'): - for name, value in module.params.get('tags').items(): - tags.append({'Key': name, 'Value': str(value)}) - return tags - - -def create_tags(pcx_id, client, module): - try: - delete_tags(pcx_id, client, module) - client.create_tags(Resources=[pcx_id], Tags=load_tags(module)) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - -def delete_tags(pcx_id, client, module): - try: - client.delete_tags(Resources=[pcx_id]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - -def find_pcx_by_id(pcx_id, client, module): - try: - return client.describe_vpc_peering_connections(VpcPeeringConnectionIds=[pcx_id]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - vpc_id=dict(), - peer_vpc_id=dict(), - peer_region=dict(), - peering_id=dict(), - peer_owner_id=dict(), - tags=dict(required=False, type='dict'), - profile=dict(), - state=dict(default='present', choices=['present', 'absent', 'accept', 'reject']) - ) - ) - required_if = [ - ('state', 'present', ['vpc_id', 'peer_vpc_id']), - ('state', 'accept', ['peering_id']), - ('state', 'reject', ['peering_id']) - ] - - module = AnsibleModule(argument_spec=argument_spec, required_if=required_if) - - if not HAS_BOTO3: - module.fail_json(msg='json, botocore and boto3 are required.') - state = module.params.get('state') - peering_id = module.params.get('peering_id') - vpc_id = module.params.get('vpc_id') - peer_vpc_id = module.params.get('peer_vpc_id') - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ec2', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - " + str(e)) - - if state == 'present': - (changed, results) = create_peer_connection(client, module) - module.exit_json(changed=changed, peering_id=results) - elif state == 'absent': - if not peering_id and (not vpc_id or not peer_vpc_id): - module.fail_json(msg='state is absent but one of the following is missing: peering_id or [vpc_id, peer_vpc_id]') - - remove_peer_connection(client, module) - else: - (changed, results) = accept_reject(state, client, module) - module.exit_json(changed=changed, peering_id=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_peering_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_peering_info.py deleted file mode 100644 index 13a22a3e3a..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_peering_info.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: ec2_vpc_peering_info -short_description: Retrieves AWS VPC Peering details using AWS methods. -description: - - Gets various details related to AWS VPC Peers - - This module was called C(ec2_vpc_peering_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -options: - peer_connection_ids: - description: - - List of specific VPC peer IDs to get details for. - type: list - elements: str - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcPeeringConnections.html) - for possible filters. - type: dict -author: Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple example of listing all VPC Peers -- name: List all vpc peers - ec2_vpc_peering_info: - region: ap-southeast-2 - register: all_vpc_peers - -- name: Debugging the result - debug: - msg: "{{ all_vpc_peers.result }}" - -- name: Get details on specific VPC peer - ec2_vpc_peering_info: - peer_connection_ids: - - pcx-12345678 - - pcx-87654321 - region: ap-southeast-2 - register: all_vpc_peers - -- name: Get all vpc peers with specific filters - ec2_vpc_peering_info: - region: ap-southeast-2 - filters: - status-code: ['pending-acceptance'] - register: pending_vpc_peers -''' - -RETURN = ''' -result: - description: The result of the describe. - returned: success - type: list -''' - -import json - -try: - import botocore -except ImportError: - pass # will be picked up by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_tag_list_to_ansible_dict, - ec2_argument_spec, boto3_conn, get_aws_connection_info, - ansible_dict_to_boto3_filter_list, HAS_BOTO3, camel_dict_to_snake_dict) - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def get_vpc_peers(client, module): - params = dict() - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - if module.params.get('peer_connection_ids'): - params['VpcPeeringConnectionIds'] = module.params.get('peer_connection_ids') - try: - result = json.loads(json.dumps(client.describe_vpc_peering_connections(**params), default=date_handler)) - except Exception as e: - module.fail_json(msg=str(e.message)) - - return result['VpcPeeringConnections'] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(default=dict(), type='dict'), - peer_connection_ids=dict(default=None, type='list'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_vpc_peering_facts': - module.deprecate("The 'ec2_vpc_peering_facts' module has been renamed to 'ec2_vpc_peering_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='botocore and boto3 are required.') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - except NameError as e: - # Getting around the get_aws_connection_info boto reliance for region - if "global name 'boto' is not defined" in e.message: - module.params['region'] = botocore.session.get_session().get_config_variable('region') - if not module.params['region']: - module.fail_json(msg="Error - no region provided") - else: - module.fail_json(msg="Can't retrieve connection information - " + str(e)) - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg=str(e)) - - # Turn the boto3 result in to ansible friendly_snaked_names - results = [camel_dict_to_snake_dict(peer) for peer in get_vpc_peers(ec2, module)] - - # Turn the boto3 result in to ansible friendly tag dictionary - for peer in results: - peer['tags'] = boto3_tag_list_to_ansible_dict(peer.get('tags', [])) - - module.exit_json(result=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py deleted file mode 100644 index 96c9b2d04d..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py +++ /dev/null @@ -1,750 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_route_table -short_description: Manage route tables for AWS virtual private clouds -description: - - Manage route tables for AWS virtual private clouds -version_added: "2.0" -author: -- Robert Estelle (@erydo) -- Rob White (@wimnat) -- Will Thames (@willthames) -options: - lookup: - description: Look up route table by either tags or by route table ID. Non-unique tag lookup will fail. - If no tags are specified then no lookup for an existing route table is performed and a new - route table will be created. To change tags of a route table you must look up by id. - default: tag - choices: [ 'tag', 'id' ] - type: str - propagating_vgw_ids: - description: Enable route propagation from virtual gateways specified by ID. - type: list - elements: str - purge_routes: - version_added: "2.3" - description: Purge existing routes that are not found in routes. - type: bool - default: 'yes' - purge_subnets: - version_added: "2.3" - description: Purge existing subnets that are not found in subnets. Ignored unless the subnets option is supplied. - default: 'true' - type: bool - purge_tags: - version_added: "2.5" - description: Purge existing tags that are not found in route table. - type: bool - default: 'no' - route_table_id: - description: - - The ID of the route table to update or delete. - - Required when I(lookup=id). - type: str - routes: - description: List of routes in the route table. - Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', - 'instance_id', 'network_interface_id', or 'vpc_peering_connection_id'. - If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. - Routes are required for present states. - type: list - elements: dict - state: - description: Create or destroy the VPC route table. - default: present - choices: [ 'present', 'absent' ] - type: str - subnets: - description: An array of subnets to add to this route table. Subnets may be specified - by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. - type: list - elements: str - tags: - description: > - A dictionary of resource tags of the form: C({ tag1: value1, tag2: value2 }). Tags are - used to uniquely identify route tables within a VPC when the route_table_id is not supplied. - aliases: [ "resource_tags" ] - type: dict - vpc_id: - description: - - VPC ID of the VPC in which to create the route table. - - Required when I(state=present) or I(lookup=tag). - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic creation example: -- name: Set up public subnet route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - tags: - Name: Public - subnets: - - "{{ jumpbox_subnet.subnet.id }}" - - "{{ frontend_subnet.subnet.id }}" - - "{{ vpn_subnet.subnet_id }}" - routes: - - dest: 0.0.0.0/0 - gateway_id: "{{ igw.gateway_id }}" - register: public_route_table - -- name: Set up NAT-protected route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - tags: - Name: Internal - subnets: - - "{{ application_subnet.subnet.id }}" - - 'Database Subnet' - - '10.0.0.0/8' - routes: - - dest: 0.0.0.0/0 - instance_id: "{{ nat.instance_id }}" - register: nat_route_table - -- name: delete route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - route_table_id: "{{ route_table.id }}" - lookup: id - state: absent -''' - -RETURN = ''' -route_table: - description: Route Table result - returned: always - type: complex - contains: - associations: - description: List of subnets associated with the route table - returned: always - type: complex - contains: - main: - description: Whether this is the main route table - returned: always - type: bool - sample: false - route_table_association_id: - description: ID of association between route table and subnet - returned: always - type: str - sample: rtbassoc-ab47cfc3 - route_table_id: - description: ID of the route table - returned: always - type: str - sample: rtb-bf779ed7 - subnet_id: - description: ID of the subnet - returned: always - type: str - sample: subnet-82055af9 - id: - description: ID of the route table (same as route_table_id for backwards compatibility) - returned: always - type: str - sample: rtb-bf779ed7 - propagating_vgws: - description: List of Virtual Private Gateways propagating routes - returned: always - type: list - sample: [] - route_table_id: - description: ID of the route table - returned: always - type: str - sample: rtb-bf779ed7 - routes: - description: List of routes in the route table - returned: always - type: complex - contains: - destination_cidr_block: - description: CIDR block of destination - returned: always - type: str - sample: 10.228.228.0/22 - gateway_id: - description: ID of the gateway - returned: when gateway is local or internet gateway - type: str - sample: local - instance_id: - description: ID of a NAT instance - returned: when the route is via an EC2 instance - type: str - sample: i-abcd123456789 - instance_owner_id: - description: AWS account owning the NAT instance - returned: when the route is via an EC2 instance - type: str - sample: 123456789012 - nat_gateway_id: - description: ID of the NAT gateway - returned: when the route is via a NAT gateway - type: str - sample: local - origin: - description: mechanism through which the route is in the table - returned: always - type: str - sample: CreateRouteTable - state: - description: state of the route - returned: always - type: str - sample: active - tags: - description: Tags applied to the route table - returned: always - type: dict - sample: - Name: Public route table - Public: 'true' - vpc_id: - description: ID for the VPC in which the route lives - returned: always - type: str - sample: vpc-6e2d2407 -''' - -import re -from time import sleep -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.ec2 import compare_aws_tags, AWSRetry - - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -CIDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$') -SUBNET_RE = re.compile(r'^subnet-[A-z0-9]+$') -ROUTE_TABLE_RE = re.compile(r'^rtb-[A-z0-9]+$') - - -@AWSRetry.exponential_backoff() -def describe_subnets_with_backoff(connection, **params): - return connection.describe_subnets(**params)['Subnets'] - - -def find_subnets(connection, module, vpc_id, identified_subnets): - """ - Finds a list of subnets, each identified either by a raw ID, a unique - 'Name' tag, or a CIDR such as 10.0.0.0/8. - - Note that this function is duplicated in other ec2 modules, and should - potentially be moved into a shared module_utils - """ - subnet_ids = [] - subnet_names = [] - subnet_cidrs = [] - for subnet in (identified_subnets or []): - if re.match(SUBNET_RE, subnet): - subnet_ids.append(subnet) - elif re.match(CIDR_RE, subnet): - subnet_cidrs.append(subnet) - else: - subnet_names.append(subnet) - - subnets_by_id = [] - if subnet_ids: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) - try: - subnets_by_id = describe_subnets_with_backoff(connection, SubnetIds=subnet_ids, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with id %s" % subnet_ids) - - subnets_by_cidr = [] - if subnet_cidrs: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr': subnet_cidrs}) - try: - subnets_by_cidr = describe_subnets_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with cidr %s" % subnet_cidrs) - - subnets_by_name = [] - if subnet_names: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'tag:Name': subnet_names}) - try: - subnets_by_name = describe_subnets_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with names %s" % subnet_names) - - for name in subnet_names: - matching_count = len([1 for s in subnets_by_name for t in s.get('Tags', []) if t['Key'] == 'Name' and t['Value'] == name]) - if matching_count == 0: - module.fail_json(msg='Subnet named "{0}" does not exist'.format(name)) - elif matching_count > 1: - module.fail_json(msg='Multiple subnets named "{0}"'.format(name)) - - return subnets_by_id + subnets_by_cidr + subnets_by_name - - -def find_igw(connection, module, vpc_id): - """ - Finds the Internet gateway for the given VPC ID. - """ - filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id}) - try: - igw = connection.describe_internet_gateways(Filters=filters)['InternetGateways'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='No IGW found for VPC {0}'.format(vpc_id)) - if len(igw) == 1: - return igw[0]['InternetGatewayId'] - elif len(igw) == 0: - module.fail_json(msg='No IGWs found for VPC {0}'.format(vpc_id)) - else: - module.fail_json(msg='Multiple IGWs found for VPC {0}'.format(vpc_id)) - - -@AWSRetry.exponential_backoff() -def describe_tags_with_backoff(connection, resource_id): - filters = ansible_dict_to_boto3_filter_list({'resource-id': resource_id}) - paginator = connection.get_paginator('describe_tags') - tags = paginator.paginate(Filters=filters).build_full_result()['Tags'] - return boto3_tag_list_to_ansible_dict(tags) - - -def tags_match(match_tags, candidate_tags): - return all((k in candidate_tags and candidate_tags[k] == v - for k, v in match_tags.items())) - - -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_aws(e, msg='Unable to list tags for VPC') - - to_add, to_delete = compare_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_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_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_aws(e, msg='Unable to list tags for VPC') - return {'changed': True, 'tags': latest_tags} - - -@AWSRetry.exponential_backoff() -def describe_route_tables_with_backoff(connection, **params): - try: - return connection.describe_route_tables(**params)['RouteTables'] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidRouteTableID.NotFound': - return None - else: - raise - - -def get_route_table_by_id(connection, module, route_table_id): - - route_table = None - try: - route_tables = describe_route_tables_with_backoff(connection, RouteTableIds=[route_table_id]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route table") - if route_tables: - route_table = route_tables[0] - - return route_table - - -def get_route_table_by_tags(connection, module, vpc_id, tags): - count = 0 - route_table = None - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) - try: - route_tables = describe_route_tables_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route table") - for table in route_tables: - this_tags = describe_tags_with_backoff(connection, table['RouteTableId']) - if tags_match(tags, this_tags): - route_table = table - count += 1 - - if count > 1: - module.fail_json(msg="Tags provided do not identify a unique route table") - else: - return route_table - - -def route_spec_matches_route(route_spec, route): - if route_spec.get('GatewayId') and 'nat-' in route_spec['GatewayId']: - route_spec['NatGatewayId'] = route_spec.pop('GatewayId') - if route_spec.get('GatewayId') and 'vpce-' in route_spec['GatewayId']: - if route_spec.get('DestinationCidrBlock', '').startswith('pl-'): - route_spec['DestinationPrefixListId'] = route_spec.pop('DestinationCidrBlock') - - return set(route_spec.items()).issubset(route.items()) - - -def route_spec_matches_route_cidr(route_spec, route): - return route_spec['DestinationCidrBlock'] == route.get('DestinationCidrBlock') - - -def rename_key(d, old_key, new_key): - d[new_key] = d.pop(old_key) - - -def index_of_matching_route(route_spec, routes_to_match): - for i, route in enumerate(routes_to_match): - if route_spec_matches_route(route_spec, route): - return "exact", i - elif 'Origin' in route_spec and route_spec['Origin'] != 'EnableVgwRoutePropagation': - if route_spec_matches_route_cidr(route_spec, route): - return "replace", i - - -def ensure_routes(connection=None, module=None, route_table=None, route_specs=None, - propagating_vgw_ids=None, check_mode=None, purge_routes=None): - routes_to_match = [route for route in route_table['Routes']] - route_specs_to_create = [] - route_specs_to_recreate = [] - for route_spec in route_specs: - match = index_of_matching_route(route_spec, routes_to_match) - if match is None: - if route_spec.get('DestinationCidrBlock'): - route_specs_to_create.append(route_spec) - else: - module.warn("Skipping creating {0} because it has no destination cidr block. " - "To add VPC endpoints to route tables use the ec2_vpc_endpoint module.".format(route_spec)) - else: - if match[0] == "replace": - if route_spec.get('DestinationCidrBlock'): - route_specs_to_recreate.append(route_spec) - else: - module.warn("Skipping recreating route {0} because it has no destination cidr block.".format(route_spec)) - del routes_to_match[match[1]] - - routes_to_delete = [] - if purge_routes: - for r in routes_to_match: - if not r.get('DestinationCidrBlock'): - module.warn("Skipping purging route {0} because it has no destination cidr block. " - "To remove VPC endpoints from route tables use the ec2_vpc_endpoint module.".format(r)) - continue - if r['Origin'] == 'CreateRoute': - routes_to_delete.append(r) - - changed = bool(routes_to_delete or route_specs_to_create or route_specs_to_recreate) - if changed and not check_mode: - for route in routes_to_delete: - try: - connection.delete_route(RouteTableId=route_table['RouteTableId'], DestinationCidrBlock=route['DestinationCidrBlock']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete route") - - for route_spec in route_specs_to_recreate: - try: - connection.replace_route(RouteTableId=route_table['RouteTableId'], - **route_spec) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't recreate route") - - for route_spec in route_specs_to_create: - try: - connection.create_route(RouteTableId=route_table['RouteTableId'], - **route_spec) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create route") - - return {'changed': bool(changed)} - - -def ensure_subnet_association(connection=None, module=None, vpc_id=None, route_table_id=None, subnet_id=None, - check_mode=None): - filters = ansible_dict_to_boto3_filter_list({'association.subnet-id': subnet_id, 'vpc-id': vpc_id}) - try: - route_tables = describe_route_tables_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route tables") - for route_table in route_tables: - if route_table['RouteTableId'] is None: - continue - for a in route_table['Associations']: - if a['Main']: - continue - if a['SubnetId'] == subnet_id: - if route_table['RouteTableId'] == route_table_id: - return {'changed': False, 'association_id': a['RouteTableAssociationId']} - else: - if check_mode: - return {'changed': True} - try: - connection.disassociate_route_table(AssociationId=a['RouteTableAssociationId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") - - try: - association_id = connection.associate_route_table(RouteTableId=route_table_id, SubnetId=subnet_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't associate subnet with route table") - return {'changed': True, 'association_id': association_id} - - -def ensure_subnet_associations(connection=None, module=None, route_table=None, subnets=None, - check_mode=None, purge_subnets=None): - current_association_ids = [a['RouteTableAssociationId'] for a in route_table['Associations'] if not a['Main']] - new_association_ids = [] - changed = False - for subnet in subnets: - result = ensure_subnet_association(connection=connection, module=module, vpc_id=route_table['VpcId'], - route_table_id=route_table['RouteTableId'], subnet_id=subnet['SubnetId'], check_mode=check_mode) - changed = changed or result['changed'] - if changed and check_mode: - return {'changed': True} - new_association_ids.append(result['association_id']) - - if purge_subnets: - to_delete = [a_id for a_id in current_association_ids - if a_id not in new_association_ids] - - for a_id in to_delete: - changed = True - if not check_mode: - try: - connection.disassociate_route_table(AssociationId=a_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") - - return {'changed': changed} - - -def ensure_propagation(connection=None, module=None, route_table=None, propagating_vgw_ids=None, - check_mode=None): - changed = False - gateways = [gateway['GatewayId'] for gateway in route_table['PropagatingVgws']] - to_add = set(propagating_vgw_ids) - set(gateways) - if to_add: - changed = True - if not check_mode: - for vgw_id in to_add: - try: - connection.enable_vgw_route_propagation(RouteTableId=route_table['RouteTableId'], - GatewayId=vgw_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't enable route propagation") - - return {'changed': changed} - - -def ensure_route_table_absent(connection, module): - - lookup = module.params.get('lookup') - route_table_id = module.params.get('route_table_id') - tags = module.params.get('tags') - vpc_id = module.params.get('vpc_id') - purge_subnets = module.params.get('purge_subnets') - - if lookup == 'tag': - if tags is not None: - route_table = get_route_table_by_tags(connection, module, vpc_id, tags) - else: - route_table = None - elif lookup == 'id': - route_table = get_route_table_by_id(connection, module, route_table_id) - - if route_table is None: - return {'changed': False} - - # disassociate subnets before deleting route table - if not module.check_mode: - ensure_subnet_associations(connection=connection, module=module, route_table=route_table, - subnets=[], check_mode=False, purge_subnets=purge_subnets) - try: - connection.delete_route_table(RouteTableId=route_table['RouteTableId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error deleting route table") - - return {'changed': True} - - -def get_route_table_info(connection, module, route_table): - result = get_route_table_by_id(connection, module, route_table['RouteTableId']) - try: - result['Tags'] = describe_tags_with_backoff(connection, route_table['RouteTableId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get tags for route table") - result = camel_dict_to_snake_dict(result, ignore_list=['Tags']) - # backwards compatibility - result['id'] = result['route_table_id'] - return result - - -def create_route_spec(connection, module, vpc_id): - routes = module.params.get('routes') - - for route_spec in routes: - rename_key(route_spec, 'dest', 'destination_cidr_block') - - if route_spec.get('gateway_id') and route_spec['gateway_id'].lower() == 'igw': - igw = find_igw(connection, module, vpc_id) - route_spec['gateway_id'] = igw - if route_spec.get('gateway_id') and route_spec['gateway_id'].startswith('nat-'): - rename_key(route_spec, 'gateway_id', 'nat_gateway_id') - - return snake_dict_to_camel_dict(routes, capitalize_first=True) - - -def ensure_route_table_present(connection, module): - - lookup = module.params.get('lookup') - propagating_vgw_ids = module.params.get('propagating_vgw_ids') - purge_routes = module.params.get('purge_routes') - purge_subnets = module.params.get('purge_subnets') - purge_tags = module.params.get('purge_tags') - route_table_id = module.params.get('route_table_id') - subnets = module.params.get('subnets') - tags = module.params.get('tags') - vpc_id = module.params.get('vpc_id') - routes = create_route_spec(connection, module, vpc_id) - - changed = False - tags_valid = False - - if lookup == 'tag': - if tags is not None: - try: - route_table = get_route_table_by_tags(connection, module, vpc_id, tags) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error finding route table with lookup 'tag'") - else: - route_table = None - elif lookup == 'id': - try: - route_table = get_route_table_by_id(connection, module, route_table_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error finding route table with lookup 'id'") - - # If no route table returned then create new route table - if route_table is None: - changed = True - if not module.check_mode: - try: - route_table = connection.create_route_table(VpcId=vpc_id)['RouteTable'] - # try to wait for route table to be present before moving on - get_waiter( - connection, 'route_table_exists' - ).wait( - RouteTableIds=[route_table['RouteTableId']], - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error creating route table") - else: - route_table = {"id": "rtb-xxxxxxxx", "route_table_id": "rtb-xxxxxxxx", "vpc_id": vpc_id} - module.exit_json(changed=changed, route_table=route_table) - - if routes is not None: - result = ensure_routes(connection=connection, module=module, route_table=route_table, - route_specs=routes, propagating_vgw_ids=propagating_vgw_ids, - check_mode=module.check_mode, purge_routes=purge_routes) - changed = changed or result['changed'] - - if propagating_vgw_ids is not None: - result = ensure_propagation(connection=connection, module=module, route_table=route_table, - propagating_vgw_ids=propagating_vgw_ids, check_mode=module.check_mode) - changed = changed or result['changed'] - - if not tags_valid and tags is not None: - result = ensure_tags(connection=connection, module=module, resource_id=route_table['RouteTableId'], tags=tags, - purge_tags=purge_tags, check_mode=module.check_mode) - route_table['Tags'] = result['tags'] - changed = changed or result['changed'] - - if subnets is not None: - associated_subnets = find_subnets(connection, module, vpc_id, subnets) - - result = ensure_subnet_associations(connection=connection, module=module, route_table=route_table, - subnets=associated_subnets, check_mode=module.check_mode, - purge_subnets=purge_subnets) - changed = changed or result['changed'] - - if changed: - # pause to allow route table routes/subnets/associations to be updated before exiting with final state - sleep(5) - module.exit_json(changed=changed, route_table=get_route_table_info(connection, module, route_table)) - - -def main(): - argument_spec = dict( - lookup=dict(default='tag', choices=['tag', 'id']), - propagating_vgw_ids=dict(type='list'), - purge_routes=dict(default=True, type='bool'), - purge_subnets=dict(default=True, type='bool'), - purge_tags=dict(default=False, type='bool'), - route_table_id=dict(), - routes=dict(default=[], type='list'), - state=dict(default='present', choices=['present', 'absent']), - subnets=dict(type='list'), - tags=dict(type='dict', aliases=['resource_tags']), - vpc_id=dict() - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['lookup', 'id', ['route_table_id']], - ['lookup', 'tag', ['vpc_id']], - ['state', 'present', ['vpc_id']]], - supports_check_mode=True) - - connection = module.client('ec2') - - state = module.params.get('state') - - if state == 'present': - result = ensure_route_table_present(connection, module) - elif state == 'absent': - result = ensure_route_table_absent(connection, module) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table_info.py deleted file mode 100644 index c3b4004608..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table_info.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_route_table_info -short_description: Gather information about ec2 VPC route tables in AWS -description: - - Gather information about ec2 VPC route tables in AWS - - This module was called C(ec2_vpc_route_table_facts) before Ansible 2.9. The usage did not change. -version_added: "2.0" -author: "Rob White (@wimnat)" -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRouteTables.html) for possible filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all VPC route tables -- ec2_vpc_route_table_info: - -# Gather information about a particular VPC route table using route table ID -- ec2_vpc_route_table_info: - filters: - route-table-id: rtb-00112233 - -# Gather information about any VPC route table with a tag key Name and value Example -- ec2_vpc_route_table_info: - filters: - "tag:Name": Example - -# Gather information about any VPC route table within VPC with ID vpc-abcdef00 -- ec2_vpc_route_table_info: - filters: - vpc-id: vpc-abcdef00 - -''' - -try: - import boto.vpc - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info - - -def get_route_table_info(route_table): - - # Add any routes to array - routes = [] - associations = [] - for route in route_table.routes: - routes.append(route.__dict__) - for association in route_table.associations: - associations.append(association.__dict__) - - route_table_info = {'id': route_table.id, - 'routes': routes, - 'associations': associations, - 'tags': route_table.tags, - 'vpc_id': route_table.vpc_id - } - - return route_table_info - - -def list_ec2_vpc_route_tables(connection, module): - - filters = module.params.get("filters") - route_table_dict_array = [] - - try: - all_route_tables = connection.get_all_route_tables(filters=filters) - except BotoServerError as e: - module.fail_json(msg=e.message) - - for route_table in all_route_tables: - route_table_dict_array.append(get_route_table_info(route_table)) - - module.exit_json(route_tables=route_table_dict_array) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(default=None, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_vpc_route_table_facts': - module.deprecate("The 'ec2_vpc_route_table_facts' module has been renamed to 'ec2_vpc_route_table_info'", version='2.13') - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region: - try: - connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") - - list_ec2_vpc_route_tables(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw.py deleted file mode 100644 index 6bcc007c7f..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw.py +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: ec2_vpc_vgw -short_description: Create and delete AWS VPN Virtual Gateways. -description: - - Creates AWS VPN Virtual Gateways - - Deletes AWS VPN Virtual Gateways - - Attaches Virtual Gateways to VPCs - - Detaches Virtual Gateways from VPCs -version_added: "2.2" -requirements: [ boto3 ] -options: - state: - description: - - present to ensure resource is created. - - absent to remove resource - default: present - choices: [ "present", "absent"] - type: str - name: - description: - - name of the vgw to be created or deleted - type: str - type: - description: - - type of the virtual gateway to be created - choices: [ "ipsec.1" ] - default: "ipsec.1" - type: str - vpn_gateway_id: - description: - - vpn gateway id of an existing virtual gateway - type: str - vpc_id: - description: - - the vpc-id of a vpc to attach or detach - type: str - asn: - description: - - the BGP ASN of the amazon side - version_added: "2.6" - type: int - wait_timeout: - description: - - number of seconds to wait for status during vpc attach and detach - default: 320 - type: int - tags: - description: - - dictionary of resource tags - aliases: [ "resource_tags" ] - type: dict -author: Nick Aslanidis (@naslanidis) -extends_documentation_fragment: - - ec2 - - aws -''' - -EXAMPLES = ''' -- name: Create a new vgw attached to a specific VPC - ec2_vpc_vgw: - state: present - region: ap-southeast-2 - profile: personal - vpc_id: vpc-12345678 - name: personal-testing - type: ipsec.1 - register: created_vgw - -- name: Create a new unattached vgw - ec2_vpc_vgw: - state: present - region: ap-southeast-2 - profile: personal - name: personal-testing - type: ipsec.1 - tags: - environment: production - owner: ABC - register: created_vgw - -- name: Remove a new vgw using the name - ec2_vpc_vgw: - state: absent - region: ap-southeast-2 - profile: personal - name: personal-testing - type: ipsec.1 - register: deleted_vgw - -- name: Remove a new vgw using the vpn_gateway_id - ec2_vpc_vgw: - state: absent - region: ap-southeast-2 - profile: personal - vpn_gateway_id: vgw-3a9aa123 - register: deleted_vgw -''' - -RETURN = ''' -result: - description: The result of the create, or delete action. - returned: success - type: dict -''' - -import time -import traceback - -try: - import botocore - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.aws.core import is_boto3_error_code -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, boto3_conn, ec2_argument_spec, get_aws_connection_info, AWSRetry -from ansible.module_utils._text import to_native - - -def get_vgw_info(vgws): - if not isinstance(vgws, list): - return - - for vgw in vgws: - vgw_info = { - 'id': vgw['VpnGatewayId'], - 'type': vgw['Type'], - 'state': vgw['State'], - 'vpc_id': None, - 'tags': dict() - } - - for tag in vgw['Tags']: - vgw_info['tags'][tag['Key']] = tag['Value'] - - if len(vgw['VpcAttachments']) != 0 and vgw['VpcAttachments'][0]['State'] == 'attached': - vgw_info['vpc_id'] = vgw['VpcAttachments'][0]['VpcId'] - - return vgw_info - - -def wait_for_status(client, module, vpn_gateway_id, status): - polling_increment_secs = 15 - max_retries = (module.params.get('wait_timeout') // polling_increment_secs) - status_achieved = False - - for x in range(0, max_retries): - try: - response = find_vgw(client, module, vpn_gateway_id) - if response[0]['VpcAttachments'][0]['State'] == status: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return status_achieved, result - - -def attach_vgw(client, module, vpn_gateway_id): - params = dict() - params['VpcId'] = module.params.get('vpc_id') - - try: - # Immediately after a detachment, the EC2 API sometimes will report the VpnGateways[0].State - # as available several seconds before actually permitting a new attachment. - # So we catch and retry that error. See https://github.com/ansible/ansible/issues/53185 - response = AWSRetry.jittered_backoff(retries=5, - catch_extra_error_codes=['InvalidParameterValue'] - )(client.attach_vpn_gateway)(VpnGatewayId=vpn_gateway_id, - VpcId=params['VpcId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'attached') - if not status_achieved: - module.fail_json(msg='Error waiting for vpc to attach to vgw - please check the AWS console') - - result = response - return result - - -def detach_vgw(client, module, vpn_gateway_id, vpc_id=None): - params = dict() - params['VpcId'] = module.params.get('vpc_id') - - if vpc_id: - try: - response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=vpc_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - else: - try: - response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=params['VpcId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'detached') - if not status_achieved: - module.fail_json(msg='Error waiting for vpc to detach from vgw - please check the AWS console') - - result = response - return result - - -def create_vgw(client, module): - params = dict() - params['Type'] = module.params.get('type') - if module.params.get('asn'): - params['AmazonSideAsn'] = module.params.get('asn') - - try: - response = client.create_vpn_gateway(**params) - get_waiter( - client, 'vpn_gateway_exists' - ).wait( - VpnGatewayIds=[response['VpnGateway']['VpnGatewayId']] - ) - except botocore.exceptions.WaiterError as e: - module.fail_json(msg="Failed to wait for Vpn Gateway {0} to be available".format(response['VpnGateway']['VpnGatewayId']), - exception=traceback.format_exc()) - except is_boto3_error_code('VpnGatewayLimitExceeded'): - module.fail_json(msg="Too many VPN gateways exist in this account.", exception=traceback.format_exc()) - except botocore.exceptions.ClientError as e: # pylint: disable=duplicate-except - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def delete_vgw(client, module, vpn_gateway_id): - - try: - response = client.delete_vpn_gateway(VpnGatewayId=vpn_gateway_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - # return the deleted VpnGatewayId as this is not included in the above response - result = vpn_gateway_id - return result - - -def create_tags(client, module, vpn_gateway_id): - params = dict() - - try: - response = client.create_tags(Resources=[vpn_gateway_id], Tags=load_tags(module)) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def delete_tags(client, module, vpn_gateway_id, tags_to_delete=None): - params = dict() - - if tags_to_delete: - try: - response = client.delete_tags(Resources=[vpn_gateway_id], Tags=tags_to_delete) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - else: - try: - response = client.delete_tags(Resources=[vpn_gateway_id]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def load_tags(module): - tags = [] - - if module.params.get('tags'): - for name, value in module.params.get('tags').items(): - tags.append({'Key': name, 'Value': str(value)}) - tags.append({'Key': "Name", 'Value': module.params.get('name')}) - else: - tags.append({'Key': "Name", 'Value': module.params.get('name')}) - return tags - - -def find_tags(client, module, resource_id=None): - - if resource_id: - try: - response = client.describe_tags(Filters=[ - {'Name': 'resource-id', 'Values': [resource_id]} - ]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def check_tags(client, module, existing_vgw, vpn_gateway_id): - params = dict() - params['Tags'] = module.params.get('tags') - vgw = existing_vgw - changed = False - tags_list = {} - - # format tags for comparison - for tags in existing_vgw[0]['Tags']: - if tags['Key'] != 'Name': - tags_list[tags['Key']] = tags['Value'] - - # if existing tags don't match the tags arg, delete existing and recreate with new list - if params['Tags'] is not None and tags_list != params['Tags']: - delete_tags(client, module, vpn_gateway_id) - create_tags(client, module, vpn_gateway_id) - vgw = find_vgw(client, module) - changed = True - - # if no tag args are supplied, delete any existing tags with the exception of the name tag - if params['Tags'] is None and tags_list != {}: - tags_to_delete = [] - for tags in existing_vgw[0]['Tags']: - if tags['Key'] != 'Name': - tags_to_delete.append(tags) - - delete_tags(client, module, vpn_gateway_id, tags_to_delete) - vgw = find_vgw(client, module) - changed = True - - return vgw, changed - - -def find_vpc(client, module): - params = dict() - params['vpc_id'] = module.params.get('vpc_id') - - if params['vpc_id']: - try: - response = client.describe_vpcs(VpcIds=[params['vpc_id']]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - result = response - return result - - -def find_vgw(client, module, vpn_gateway_id=None): - params = dict() - if vpn_gateway_id: - params['VpnGatewayIds'] = vpn_gateway_id - else: - params['Filters'] = [ - {'Name': 'type', 'Values': [module.params.get('type')]}, - {'Name': 'tag:Name', 'Values': [module.params.get('name')]}, - ] - if module.params.get('state') == 'present': - params['Filters'].append({'Name': 'state', 'Values': ['pending', 'available']}) - try: - response = client.describe_vpn_gateways(**params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - return sorted(response['VpnGateways'], key=lambda k: k['VpnGatewayId']) - - -def ensure_vgw_present(client, module): - - # If an existing vgw name and type matches our args, then a match is considered to have been - # found and we will not create another vgw. - - changed = False - params = dict() - result = dict() - params['Name'] = module.params.get('name') - params['VpcId'] = module.params.get('vpc_id') - params['Type'] = module.params.get('type') - params['Tags'] = module.params.get('tags') - params['VpnGatewayIds'] = module.params.get('vpn_gateway_id') - - # check that the vpc_id exists. If not, an exception is thrown - if params['VpcId']: - vpc = find_vpc(client, module) - - # check if a gateway matching our module args already exists - existing_vgw = find_vgw(client, module) - - if existing_vgw != []: - vpn_gateway_id = existing_vgw[0]['VpnGatewayId'] - vgw, changed = check_tags(client, module, existing_vgw, vpn_gateway_id) - - # if a vpc_id was provided, check if it exists and if it's attached - if params['VpcId']: - - current_vpc_attachments = existing_vgw[0]['VpcAttachments'] - - if current_vpc_attachments != [] and current_vpc_attachments[0]['State'] == 'attached': - if current_vpc_attachments[0]['VpcId'] != params['VpcId'] or current_vpc_attachments[0]['State'] != 'attached': - # detach the existing vpc from the virtual gateway - vpc_to_detach = current_vpc_attachments[0]['VpcId'] - detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) - get_waiter(client, 'vpn_gateway_detached').wait(VpnGatewayIds=[vpn_gateway_id]) - attached_vgw = attach_vgw(client, module, vpn_gateway_id) - changed = True - else: - # attach the vgw to the supplied vpc - attached_vgw = attach_vgw(client, module, vpn_gateway_id) - changed = True - - # if params['VpcId'] is not provided, check the vgw is attached to a vpc. if so, detach it. - else: - existing_vgw = find_vgw(client, module, [vpn_gateway_id]) - - if existing_vgw[0]['VpcAttachments'] != []: - if existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': - # detach the vpc from the vgw - vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] - detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) - changed = True - - else: - # create a new vgw - new_vgw = create_vgw(client, module) - changed = True - vpn_gateway_id = new_vgw['VpnGateway']['VpnGatewayId'] - - # tag the new virtual gateway - create_tags(client, module, vpn_gateway_id) - - # if a vpc-id was supplied, attempt to attach it to the vgw - if params['VpcId']: - attached_vgw = attach_vgw(client, module, vpn_gateway_id) - changed = True - - # return current state of the vgw - vgw = find_vgw(client, module, [vpn_gateway_id]) - result = get_vgw_info(vgw) - return changed, result - - -def ensure_vgw_absent(client, module): - - # If an existing vgw name and type matches our args, then a match is considered to have been - # found and we will take steps to delete it. - - changed = False - params = dict() - result = dict() - params['Name'] = module.params.get('name') - params['VpcId'] = module.params.get('vpc_id') - params['Type'] = module.params.get('type') - params['Tags'] = module.params.get('tags') - params['VpnGatewayIds'] = module.params.get('vpn_gateway_id') - - # check if a gateway matching our module args already exists - if params['VpnGatewayIds']: - existing_vgw_with_id = find_vgw(client, module, [params['VpnGatewayIds']]) - if existing_vgw_with_id != [] and existing_vgw_with_id[0]['State'] != 'deleted': - existing_vgw = existing_vgw_with_id - if existing_vgw[0]['VpcAttachments'] != [] and existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': - if params['VpcId']: - if params['VpcId'] != existing_vgw[0]['VpcAttachments'][0]['VpcId']: - module.fail_json(msg='The vpc-id provided does not match the vpc-id currently attached - please check the AWS console') - - else: - # detach the vpc from the vgw - detach_vgw(client, module, params['VpnGatewayIds'], params['VpcId']) - deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) - changed = True - - else: - # attempt to detach any attached vpcs - vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] - detach_vgw(client, module, params['VpnGatewayIds'], vpc_to_detach) - deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) - changed = True - - else: - # no vpc's are attached so attempt to delete the vgw - deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) - changed = True - - else: - changed = False - deleted_vgw = "Nothing to do" - - else: - # Check that a name and type argument has been supplied if no vgw-id - if not module.params.get('name') or not module.params.get('type'): - module.fail_json(msg='A name and type is required when no vgw-id and a status of \'absent\' is supplied') - - existing_vgw = find_vgw(client, module) - if existing_vgw != [] and existing_vgw[0]['State'] != 'deleted': - vpn_gateway_id = existing_vgw[0]['VpnGatewayId'] - if existing_vgw[0]['VpcAttachments'] != [] and existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': - if params['VpcId']: - if params['VpcId'] != existing_vgw[0]['VpcAttachments'][0]['VpcId']: - module.fail_json(msg='The vpc-id provided does not match the vpc-id currently attached - please check the AWS console') - - else: - # detach the vpc from the vgw - detach_vgw(client, module, vpn_gateway_id, params['VpcId']) - - # now that the vpc has been detached, delete the vgw - deleted_vgw = delete_vgw(client, module, vpn_gateway_id) - changed = True - - else: - # attempt to detach any attached vpcs - vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] - detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) - changed = True - - # now that the vpc has been detached, delete the vgw - deleted_vgw = delete_vgw(client, module, vpn_gateway_id) - - else: - # no vpc's are attached so attempt to delete the vgw - deleted_vgw = delete_vgw(client, module, vpn_gateway_id) - changed = True - - else: - changed = False - deleted_vgw = None - - result = deleted_vgw - return changed, result - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(default='present', choices=['present', 'absent']), - name=dict(), - vpn_gateway_id=dict(), - vpc_id=dict(), - asn=dict(type='int'), - wait_timeout=dict(type='int', default=320), - type=dict(default='ipsec.1', choices=['ipsec.1']), - tags=dict(default=None, required=False, type='dict', aliases=['resource_tags']), - ) - ) - module = AnsibleModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['name']]]) - - if not HAS_BOTO3: - module.fail_json(msg='json and boto3 is required.') - - state = module.params.get('state').lower() - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - %s" % to_native(e), exception=traceback.format_exc()) - - if state == 'present': - (changed, results) = ensure_vgw_present(client, module) - else: - (changed, results) = ensure_vgw_absent(client, module) - module.exit_json(changed=changed, vgw=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw_info.py deleted file mode 100644 index 77d1eaea6a..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_vgw_info.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_vgw_info -short_description: Gather information about virtual gateways in AWS -description: - - Gather information about virtual gateways in AWS. - - This module was called C(ec2_vpc_vgw_facts) before Ansible 2.9. The usage did not change. -version_added: "2.3" -requirements: [ boto3 ] -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpnGateways.html) for possible filters. - type: dict - vpn_gateway_ids: - description: - - Get details of a specific Virtual Gateway ID. This value should be provided as a list. - type: list - elements: str -author: "Nick Aslanidis (@naslanidis)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# # Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Gather information about all virtual gateways for an account or profile - ec2_vpc_vgw_info: - region: ap-southeast-2 - profile: production - register: vgw_info - -- name: Gather information about a filtered list of Virtual Gateways - ec2_vpc_vgw_info: - region: ap-southeast-2 - profile: production - filters: - "tag:Name": "main-virt-gateway" - register: vgw_info - -- name: Gather information about a specific virtual gateway by VpnGatewayIds - ec2_vpc_vgw_info: - region: ap-southeast-2 - profile: production - vpn_gateway_ids: vgw-c432f6a7 - register: vgw_info -''' - -RETURN = ''' -virtual_gateways: - description: The virtual gateways for the account. - returned: always - type: list - sample: [ - { - "state": "available", - "tags": [ - { - "key": "Name", - "value": "TEST-VGW" - } - ], - "type": "ipsec.1", - "vpc_attachments": [ - { - "state": "attached", - "vpc_id": "vpc-22a93c74" - } - ], - "vpn_gateway_id": "vgw-23e3d64e" - } - ] - -changed: - description: True if listing the virtual gateways succeeds. - returned: always - type: bool - sample: "false" -''' -import traceback - -try: - import botocore -except ImportError: - pass # will be captured by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, - camel_dict_to_snake_dict, ansible_dict_to_boto3_filter_list, HAS_BOTO3) - - -def get_virtual_gateway_info(virtual_gateway): - virtual_gateway_info = {'VpnGatewayId': virtual_gateway['VpnGatewayId'], - 'State': virtual_gateway['State'], - 'Type': virtual_gateway['Type'], - 'VpcAttachments': virtual_gateway['VpcAttachments'], - 'Tags': virtual_gateway.get('Tags', [])} - return virtual_gateway_info - - -def list_virtual_gateways(client, module): - params = dict() - - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - params['DryRun'] = module.check_mode - - if module.params.get("vpn_gateway_ids"): - params['VpnGatewayIds'] = module.params.get("vpn_gateway_ids") - - try: - all_virtual_gateways = client.describe_vpn_gateways(**params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc()) - - return [camel_dict_to_snake_dict(get_virtual_gateway_info(vgw)) - for vgw in all_virtual_gateways['VpnGateways']] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(type='dict', default=dict()), - vpn_gateway_ids=dict(type='list', default=None) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vpc_vgw_facts': - module.deprecate("The 'ec2_vpc_vgw_facts' module has been renamed to 'ec2_vpc_vgw_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='json and boto3 is required.') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - " + str(e)) - - # call your function here - results = list_virtual_gateways(connection, module) - - module.exit_json(virtual_gateways=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn.py deleted file mode 100644 index 29d65326ff..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn.py +++ /dev/null @@ -1,783 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_vpc_vpn -short_description: Create, modify, and delete EC2 VPN connections. -description: - - This module creates, modifies, and deletes VPN connections. Idempotence is achieved by using the filters - option or specifying the VPN connection identifier. -version_added: "2.4" -extends_documentation_fragment: - - ec2 - - aws -requirements: ['boto3', 'botocore'] -author: "Sloane Hertel (@s-hertel)" -options: - state: - description: - - The desired state of the VPN connection. - choices: ['present', 'absent'] - default: present - required: no - type: str - customer_gateway_id: - description: - - The ID of the customer gateway. - type: str - connection_type: - description: - - The type of VPN connection. - - At this time only 'ipsec.1' is supported. - default: ipsec.1 - type: str - vpn_gateway_id: - description: - - The ID of the virtual private gateway. - type: str - vpn_connection_id: - description: - - The ID of the VPN connection. Required to modify or delete a connection if the filters option does not provide a unique match. - type: str - tags: - description: - - Tags to attach to the VPN connection. - type: dict - purge_tags: - description: - - Whether or not to delete VPN connections tags that are associated with the connection but not specified in the task. - type: bool - default: false - static_only: - description: - - Indicates whether the VPN connection uses static routes only. Static routes must be used for devices that don't support BGP. - default: False - type: bool - required: no - tunnel_options: - description: - - An optional list object containing no more than two dict members, each of which may contain 'TunnelInsideCidr' - and/or 'PreSharedKey' keys with appropriate string values. AWS defaults will apply in absence of either of - the aforementioned keys. - required: no - version_added: "2.5" - type: list - elements: dict - suboptions: - TunnelInsideCidr: - type: str - description: The range of inside IP addresses for the tunnel. - PreSharedKey: - type: str - description: The pre-shared key (PSK) to establish initial authentication between the virtual private gateway and customer gateway. - filters: - description: - - An alternative to using vpn_connection_id. If multiple matches are found, vpn_connection_id is required. - If one of the following suboptions is a list of items to filter by, only one item needs to match to find the VPN - that correlates. e.g. if the filter 'cidr' is ['194.168.2.0/24', '192.168.2.0/24'] and the VPN route only has the - destination cidr block of '192.168.2.0/24' it will be found with this filter (assuming there are not multiple - VPNs that are matched). Another example, if the filter 'vpn' is equal to ['vpn-ccf7e7ad', 'vpn-cb0ae2a2'] and one - of of the VPNs has the state deleted (exists but is unmodifiable) and the other exists and is not deleted, - it will be found via this filter. See examples. - suboptions: - cgw-config: - description: - - The customer gateway configuration of the VPN as a string (in the format of the return value) or a list of those strings. - static-routes-only: - description: - - The type of routing; true or false. - cidr: - description: - - The destination cidr of the VPN's route as a string or a list of those strings. - bgp: - description: - - The BGP ASN number associated with a BGP device. Only works if the connection is attached. - This filtering option is currently not working. - vpn: - description: - - The VPN connection id as a string or a list of those strings. - vgw: - description: - - The virtual private gateway as a string or a list of those strings. - tag-keys: - description: - - The key of a tag as a string or a list of those strings. - tag-values: - description: - - The value of a tag as a string or a list of those strings. - tags: - description: - - A dict of key value pairs. - cgw: - description: - - The customer gateway id as a string or a list of those strings. - type: dict - routes: - description: - - Routes to add to the connection. - type: list - elements: str - purge_routes: - description: - - Whether or not to delete VPN connections routes that are not specified in the task. - type: bool - wait_timeout: - description: - - How long before wait gives up, in seconds. - default: 600 - type: int - required: false - version_added: "2.8" - delay: - description: - - The time to wait before checking operation again. in seconds. - required: false - type: int - default: 15 - version_added: "2.8" -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. - -- name: create a VPN connection - ec2_vpc_vpn: - state: present - vpn_gateway_id: vgw-XXXXXXXX - customer_gateway_id: cgw-XXXXXXXX - -- name: modify VPN connection tags - ec2_vpc_vpn: - state: present - vpn_connection_id: vpn-XXXXXXXX - tags: - Name: ansible-tag-1 - Other: ansible-tag-2 - -- name: delete a connection - ec2_vpc_vpn: - vpn_connection_id: vpn-XXXXXXXX - state: absent - -- name: modify VPN tags (identifying VPN by filters) - ec2_vpc_vpn: - state: present - filters: - cidr: 194.168.1.0/24 - tag-keys: - - Ansible - - Other - tags: - New: Tag - purge_tags: true - static_only: true - -- name: set up VPN with tunnel options utilizing 'TunnelInsideCidr' only - ec2_vpc_vpn: - state: present - filters: - vpn: vpn-XXXXXXXX - static_only: true - tunnel_options: - - - TunnelInsideCidr: '169.254.100.1/30' - - - TunnelInsideCidr: '169.254.100.5/30' - -- name: add routes and remove any preexisting ones - ec2_vpc_vpn: - state: present - filters: - vpn: vpn-XXXXXXXX - routes: - - 195.168.2.0/24 - - 196.168.2.0/24 - purge_routes: true - -- name: remove all routes - ec2_vpc_vpn: - state: present - vpn_connection_id: vpn-XXXXXXXX - routes: [] - purge_routes: true - -- name: delete a VPN identified by filters - ec2_vpc_vpn: - state: absent - filters: - tags: - Ansible: Tag -""" - -RETURN = """ -changed: - description: If the VPN connection has changed. - type: bool - returned: always - sample: - changed: true -customer_gateway_configuration: - description: The configuration of the VPN connection. - returned: I(state=present) - type: str -customer_gateway_id: - description: The customer gateway connected via the connection. - type: str - returned: I(state=present) - sample: - customer_gateway_id: cgw-1220c87b -vpn_gateway_id: - description: The virtual private gateway connected via the connection. - type: str - returned: I(state=present) - sample: - vpn_gateway_id: vgw-cb0ae2a2 -options: - description: The VPN connection options (currently only containing static_routes_only). - type: complex - returned: I(state=present) - contains: - static_routes_only: - description: If the VPN connection only allows static routes. - returned: I(state=present) - type: str - sample: - static_routes_only: true -routes: - description: The routes of the VPN connection. - type: list - returned: I(state=present) - sample: - routes: [{ - 'destination_cidr_block': '192.168.1.0/24', - 'state': 'available' - }] -state: - description: The status of the VPN connection. - type: str - returned: I(state=present) - sample: - state: available -tags: - description: The tags associated with the connection. - type: dict - returned: I(state=present) - sample: - tags: - name: ansible-test - other: tag -type: - description: The type of VPN connection (currently only ipsec.1 is available). - type: str - returned: I(state=present) - sample: - type: "ipsec.1" -vgw_telemetry: - type: list - returned: I(state=present) - description: The telemetry for the VPN tunnel. - sample: - vgw_telemetry: [{ - 'outside_ip_address': 'string', - 'status': 'up', - 'last_status_change': datetime(2015, 1, 1), - 'status_message': 'string', - 'accepted_route_count': 123 - }] -vpn_connection_id: - description: The identifier for the VPN connection. - type: str - returned: I(state=present) - sample: - vpn_connection_id: vpn-781e0e19 -""" - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils._text import to_text -from ansible.module_utils.ec2 import ( - camel_dict_to_snake_dict, - boto3_tag_list_to_ansible_dict, - compare_aws_tags, - ansible_dict_to_boto3_tag_list, -) - -try: - from botocore.exceptions import BotoCoreError, ClientError, WaiterError -except ImportError: - pass # Handled by AnsibleAWSModule - - -class VPNConnectionException(Exception): - def __init__(self, msg, exception=None): - self.msg = msg - self.exception = exception - - -def find_connection(connection, module_params, vpn_connection_id=None): - ''' Looks for a unique VPN connection. Uses find_connection_response() to return the connection found, None, - or raise an error if there were multiple viable connections. ''' - - filters = module_params.get('filters') - - # vpn_connection_id may be provided via module option; takes precedence over any filter values - if not vpn_connection_id and module_params.get('vpn_connection_id'): - vpn_connection_id = module_params.get('vpn_connection_id') - - if not isinstance(vpn_connection_id, list) and vpn_connection_id: - vpn_connection_id = [to_text(vpn_connection_id)] - elif isinstance(vpn_connection_id, list): - vpn_connection_id = [to_text(connection) for connection in vpn_connection_id] - - formatted_filter = [] - # if vpn_connection_id is provided it will take precedence over any filters since it is a unique identifier - if not vpn_connection_id: - formatted_filter = create_filter(module_params, provided_filters=filters) - - # see if there is a unique matching connection - try: - if vpn_connection_id: - existing_conn = connection.describe_vpn_connections(VpnConnectionIds=vpn_connection_id, - Filters=formatted_filter) - else: - existing_conn = connection.describe_vpn_connections(Filters=formatted_filter) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed while describing VPN connection.", - exception=e) - - return find_connection_response(connections=existing_conn) - - -def add_routes(connection, vpn_connection_id, routes_to_add): - for route in routes_to_add: - try: - connection.create_vpn_connection_route(VpnConnectionId=vpn_connection_id, - DestinationCidrBlock=route) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed while adding route {0} to the VPN connection {1}.".format(route, vpn_connection_id), - exception=e) - - -def remove_routes(connection, vpn_connection_id, routes_to_remove): - for route in routes_to_remove: - try: - connection.delete_vpn_connection_route(VpnConnectionId=vpn_connection_id, - DestinationCidrBlock=route) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to remove route {0} from the VPN connection {1}.".format(route, vpn_connection_id), - exception=e) - - -def create_filter(module_params, provided_filters): - """ Creates a filter using the user-specified parameters and unmodifiable options that may have been specified in the task """ - boto3ify_filter = {'cgw-config': 'customer-gateway-configuration', - 'static-routes-only': 'option.static-routes-only', - 'cidr': 'route.destination-cidr-block', - 'bgp': 'bgp-asn', - 'vpn': 'vpn-connection-id', - 'vgw': 'vpn-gateway-id', - 'tag-keys': 'tag-key', - 'tag-values': 'tag-value', - 'tags': 'tag', - 'cgw': 'customer-gateway-id'} - - # unmodifiable options and their filter name counterpart - param_to_filter = {"customer_gateway_id": "customer-gateway-id", - "vpn_gateway_id": "vpn-gateway-id", - "vpn_connection_id": "vpn-connection-id"} - - flat_filter_dict = {} - formatted_filter = [] - - for raw_param in dict(provided_filters): - - # fix filter names to be recognized by boto3 - if raw_param in boto3ify_filter: - param = boto3ify_filter[raw_param] - provided_filters[param] = provided_filters.pop(raw_param) - elif raw_param in list(boto3ify_filter.items()): - param = raw_param - else: - raise VPNConnectionException(msg="{0} is not a valid filter.".format(raw_param)) - - # reformat filters with special formats - if param == 'tag': - for key in provided_filters[param]: - formatted_key = 'tag:' + key - if isinstance(provided_filters[param][key], list): - flat_filter_dict[formatted_key] = str(provided_filters[param][key]) - else: - flat_filter_dict[formatted_key] = [str(provided_filters[param][key])] - elif param == 'option.static-routes-only': - flat_filter_dict[param] = [str(provided_filters[param]).lower()] - else: - if isinstance(provided_filters[param], list): - flat_filter_dict[param] = provided_filters[param] - else: - flat_filter_dict[param] = [str(provided_filters[param])] - - # if customer_gateway, vpn_gateway, or vpn_connection was specified in the task but not the filter, add it - for param in param_to_filter: - if param_to_filter[param] not in flat_filter_dict and module_params.get(param): - flat_filter_dict[param_to_filter[param]] = [module_params.get(param)] - - # change the flat dict into something boto3 will understand - formatted_filter = [{'Name': key, 'Values': value} for key, value in flat_filter_dict.items()] - - return formatted_filter - - -def find_connection_response(connections=None): - """ Determine if there is a viable unique match in the connections described. Returns the unique VPN connection if one is found, - returns None if the connection does not exist, raise an error if multiple matches are found. """ - - # Found no connections - if not connections or 'VpnConnections' not in connections: - return None - - # Too many results - elif connections and len(connections['VpnConnections']) > 1: - viable = [] - for each in connections['VpnConnections']: - # deleted connections are not modifiable - if each['State'] not in ("deleted", "deleting"): - viable.append(each) - if len(viable) == 1: - # Found one viable result; return unique match - return viable[0] - elif len(viable) == 0: - # Found a result but it was deleted already; since there was only one viable result create a new one - return None - else: - raise VPNConnectionException(msg="More than one matching VPN connection was found. " - "To modify or delete a VPN please specify vpn_connection_id or add filters.") - - # Found unique match - elif connections and len(connections['VpnConnections']) == 1: - # deleted connections are not modifiable - if connections['VpnConnections'][0]['State'] not in ("deleted", "deleting"): - return connections['VpnConnections'][0] - - -def create_connection(connection, customer_gateway_id, static_only, vpn_gateway_id, connection_type, max_attempts, delay, tunnel_options=None): - """ Creates a VPN connection """ - - options = {'StaticRoutesOnly': static_only} - if tunnel_options and len(tunnel_options) <= 2: - t_opt = [] - for m in tunnel_options: - # See Boto3 docs regarding 'create_vpn_connection' - # tunnel options for allowed 'TunnelOptions' keys. - if not isinstance(m, dict): - raise TypeError("non-dict list member") - t_opt.append(m) - if t_opt: - options['TunnelOptions'] = t_opt - - if not (customer_gateway_id and vpn_gateway_id): - raise VPNConnectionException(msg="No matching connection was found. To create a new connection you must provide " - "both vpn_gateway_id and customer_gateway_id.") - try: - vpn = connection.create_vpn_connection(Type=connection_type, - CustomerGatewayId=customer_gateway_id, - VpnGatewayId=vpn_gateway_id, - Options=options) - connection.get_waiter('vpn_connection_available').wait( - VpnConnectionIds=[vpn['VpnConnection']['VpnConnectionId']], - WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts} - ) - except WaiterError as e: - raise VPNConnectionException(msg="Failed to wait for VPN connection {0} to be available".format(vpn['VpnConnection']['VpnConnectionId']), - exception=e) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to create VPN connection", - exception=e) - - return vpn['VpnConnection'] - - -def delete_connection(connection, vpn_connection_id, delay, max_attempts): - """ Deletes a VPN connection """ - try: - connection.delete_vpn_connection(VpnConnectionId=vpn_connection_id) - connection.get_waiter('vpn_connection_deleted').wait( - VpnConnectionIds=[vpn_connection_id], - WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts} - ) - except WaiterError as e: - raise VPNConnectionException(msg="Failed to wait for VPN connection {0} to be removed".format(vpn_connection_id), - exception=e) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to delete the VPN connection: {0}".format(vpn_connection_id), - exception=e) - - -def add_tags(connection, vpn_connection_id, add): - try: - connection.create_tags(Resources=[vpn_connection_id], - Tags=add) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to add the tags: {0}.".format(add), - exception=e) - - -def remove_tags(connection, vpn_connection_id, remove): - # format tags since they are a list in the format ['tag1', 'tag2', 'tag3'] - key_dict_list = [{'Key': tag} for tag in remove] - try: - connection.delete_tags(Resources=[vpn_connection_id], - Tags=key_dict_list) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to remove the tags: {0}.".format(remove), - exception=e) - - -def check_for_update(connection, module_params, vpn_connection_id): - """ Determines if there are any tags or routes that need to be updated. Ensures non-modifiable attributes aren't expected to change. """ - tags = module_params.get('tags') - routes = module_params.get('routes') - purge_tags = module_params.get('purge_tags') - purge_routes = module_params.get('purge_routes') - - vpn_connection = find_connection(connection, module_params, vpn_connection_id=vpn_connection_id) - current_attrs = camel_dict_to_snake_dict(vpn_connection) - - # Initialize changes dict - changes = {'tags_to_add': [], - 'tags_to_remove': [], - 'routes_to_add': [], - 'routes_to_remove': []} - - # Get changes to tags - current_tags = boto3_tag_list_to_ansible_dict(current_attrs.get('tags', []), u'key', u'value') - tags_to_add, changes['tags_to_remove'] = compare_aws_tags(current_tags, tags, purge_tags) - changes['tags_to_add'] = ansible_dict_to_boto3_tag_list(tags_to_add) - # Get changes to routes - if 'Routes' in vpn_connection: - current_routes = [route['DestinationCidrBlock'] for route in vpn_connection['Routes']] - if purge_routes: - changes['routes_to_remove'] = [old_route for old_route in current_routes if old_route not in routes] - changes['routes_to_add'] = [new_route for new_route in routes if new_route not in current_routes] - - # Check if nonmodifiable attributes are attempted to be modified - for attribute in current_attrs: - if attribute in ("tags", "routes", "state"): - continue - elif attribute == 'options': - will_be = module_params.get('static_only', None) - is_now = bool(current_attrs[attribute]['static_routes_only']) - attribute = 'static_only' - elif attribute == 'type': - will_be = module_params.get("connection_type", None) - is_now = current_attrs[attribute] - else: - is_now = current_attrs[attribute] - will_be = module_params.get(attribute, None) - - if will_be is not None and to_text(will_be) != to_text(is_now): - raise VPNConnectionException(msg="You cannot modify {0}, the current value of which is {1}. Modifiable VPN " - "connection attributes are tags and routes. The value you tried to change it to " - "is {2}.".format(attribute, is_now, will_be)) - - return changes - - -def make_changes(connection, vpn_connection_id, changes): - """ changes is a dict with the keys 'tags_to_add', 'tags_to_remove', 'routes_to_add', 'routes_to_remove', - the values of which are lists (generated by check_for_update()). - """ - changed = False - - if changes['tags_to_add']: - changed = True - add_tags(connection, vpn_connection_id, changes['tags_to_add']) - - if changes['tags_to_remove']: - changed = True - remove_tags(connection, vpn_connection_id, changes['tags_to_remove']) - - if changes['routes_to_add']: - changed = True - add_routes(connection, vpn_connection_id, changes['routes_to_add']) - - if changes['routes_to_remove']: - changed = True - remove_routes(connection, vpn_connection_id, changes['routes_to_remove']) - - return changed - - -def get_check_mode_results(connection, module_params, vpn_connection_id=None, current_state=None): - """ Returns the changes that would be made to a VPN Connection """ - state = module_params.get('state') - if state == 'absent': - if vpn_connection_id: - return True, {} - else: - return False, {} - - changed = False - results = {'customer_gateway_configuration': '', - 'customer_gateway_id': module_params.get('customer_gateway_id'), - 'vpn_gateway_id': module_params.get('vpn_gateway_id'), - 'options': {'static_routes_only': module_params.get('static_only')}, - 'routes': [module_params.get('routes')]} - - # get combined current tags and tags to set - present_tags = module_params.get('tags') - if current_state and 'Tags' in current_state: - current_tags = boto3_tag_list_to_ansible_dict(current_state['Tags']) - if module_params.get('purge_tags'): - if current_tags != present_tags: - changed = True - elif current_tags != present_tags: - if not set(present_tags.keys()) < set(current_tags.keys()): - changed = True - # add preexisting tags that new tags didn't overwrite - present_tags.update((tag, current_tags[tag]) for tag in current_tags if tag not in present_tags) - elif current_tags.keys() == present_tags.keys() and set(present_tags.values()) != set(current_tags.values()): - changed = True - elif module_params.get('tags'): - changed = True - if present_tags: - results['tags'] = present_tags - - # get combined current routes and routes to add - present_routes = module_params.get('routes') - if current_state and 'Routes' in current_state: - current_routes = [route['DestinationCidrBlock'] for route in current_state['Routes']] - if module_params.get('purge_routes'): - if set(current_routes) != set(present_routes): - changed = True - elif set(present_routes) != set(current_routes): - if not set(present_routes) < set(current_routes): - changed = True - present_routes.extend([route for route in current_routes if route not in present_routes]) - elif module_params.get('routes'): - changed = True - results['routes'] = [{"destination_cidr_block": cidr, "state": "available"} for cidr in present_routes] - - # return the vpn_connection_id if it's known - if vpn_connection_id: - results['vpn_connection_id'] = vpn_connection_id - else: - changed = True - results['vpn_connection_id'] = 'vpn-XXXXXXXX' - - return changed, results - - -def ensure_present(connection, module_params, check_mode=False): - """ Creates and adds tags to a VPN connection. If the connection already exists update tags. """ - vpn_connection = find_connection(connection, module_params) - changed = False - delay = module_params.get('delay') - max_attempts = module_params.get('wait_timeout') // delay - - # No match but vpn_connection_id was specified. - if not vpn_connection and module_params.get('vpn_connection_id'): - raise VPNConnectionException(msg="There is no VPN connection available or pending with that id. Did you delete it?") - - # Unique match was found. Check if attributes provided differ. - elif vpn_connection: - vpn_connection_id = vpn_connection['VpnConnectionId'] - # check_for_update returns a dict with the keys tags_to_add, tags_to_remove, routes_to_add, routes_to_remove - changes = check_for_update(connection, module_params, vpn_connection_id) - if check_mode: - return get_check_mode_results(connection, module_params, vpn_connection_id, current_state=vpn_connection) - changed = make_changes(connection, vpn_connection_id, changes) - - # No match was found. Create and tag a connection and add routes. - else: - changed = True - if check_mode: - return get_check_mode_results(connection, module_params) - vpn_connection = create_connection(connection, - customer_gateway_id=module_params.get('customer_gateway_id'), - static_only=module_params.get('static_only'), - vpn_gateway_id=module_params.get('vpn_gateway_id'), - connection_type=module_params.get('connection_type'), - tunnel_options=module_params.get('tunnel_options'), - max_attempts=max_attempts, - delay=delay) - changes = check_for_update(connection, module_params, vpn_connection['VpnConnectionId']) - make_changes(connection, vpn_connection['VpnConnectionId'], changes) - - # get latest version if a change has been made and make tags output nice before returning it - if vpn_connection: - vpn_connection = find_connection(connection, module_params, vpn_connection['VpnConnectionId']) - if 'Tags' in vpn_connection: - vpn_connection['Tags'] = boto3_tag_list_to_ansible_dict(vpn_connection['Tags']) - - return changed, vpn_connection - - -def ensure_absent(connection, module_params, check_mode=False): - """ Deletes a VPN connection if it exists. """ - vpn_connection = find_connection(connection, module_params) - - if check_mode: - return get_check_mode_results(connection, module_params, vpn_connection['VpnConnectionId'] if vpn_connection else None) - - delay = module_params.get('delay') - max_attempts = module_params.get('wait_timeout') // delay - - if vpn_connection: - delete_connection(connection, vpn_connection['VpnConnectionId'], delay=delay, max_attempts=max_attempts) - changed = True - else: - changed = False - - return changed, {} - - -def main(): - argument_spec = dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - filters=dict(type='dict', default={}), - vpn_gateway_id=dict(type='str'), - tags=dict(default={}, type='dict'), - connection_type=dict(default='ipsec.1', type='str'), - tunnel_options=dict(no_log=True, type='list', default=[]), - static_only=dict(default=False, type='bool'), - customer_gateway_id=dict(type='str'), - vpn_connection_id=dict(type='str'), - purge_tags=dict(type='bool', default=False), - routes=dict(type='list', default=[]), - purge_routes=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=600), - delay=dict(type='int', default=15), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True) - connection = module.client('ec2') - - state = module.params.get('state') - parameters = dict(module.params) - - try: - if state == 'present': - changed, response = ensure_present(connection, parameters, module.check_mode) - elif state == 'absent': - changed, response = ensure_absent(connection, parameters, module.check_mode) - except VPNConnectionException as e: - if e.exception: - module.fail_json_aws(e.exception, msg=e.msg) - else: - module.fail_json(msg=e.msg) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn_info.py deleted file mode 100644 index dbc706fc55..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_vpn_info.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_vpn_info -short_description: Gather information about VPN Connections in AWS. -description: - - Gather information about VPN Connections in AWS. - - This module was called C(ec2_vpc_vpn_facts) before Ansible 2.9. The usage did not change. -version_added: "2.6" -requirements: [ boto3 ] -author: Madhura Naniwadekar (@Madhura-CSI) -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpnConnections.html) for possible filters. - required: false - type: dict - vpn_connection_ids: - description: - - Get details of a specific VPN connections using vpn connection ID/IDs. This value should be provided as a list. - required: false - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# # Note: These examples do not set authentication details, see the AWS Guide for details. -- name: Gather information about all vpn connections - ec2_vpc_vpn_info: - -- name: Gather information about a filtered list of vpn connections, based on tags - ec2_vpc_vpn_info: - filters: - "tag:Name": test-connection - register: vpn_conn_info - -- name: Gather information about vpn connections by specifying connection IDs. - ec2_vpc_vpn_info: - filters: - vpn-gateway-id: vgw-cbe66beb - register: vpn_conn_info -''' - -RETURN = ''' -vpn_connections: - description: List of one or more VPN Connections. - returned: always - type: complex - contains: - category: - description: The category of the VPN connection. - returned: always - type: str - sample: VPN - customer_gatway_configuration: - description: The configuration information for the VPN connection's customer gateway (in the native XML format). - returned: always - type: str - customer_gateway_id: - description: The ID of the customer gateway at your end of the VPN connection. - returned: always - type: str - sample: cgw-17a53c37 - options: - description: The VPN connection options. - returned: always - type: dict - sample: { - "static_routes_only": false - } - routes: - description: List of static routes associated with the VPN connection. - returned: always - type: complex - contains: - destination_cidr_block: - description: The CIDR block associated with the local subnet of the customer data center. - returned: always - type: str - sample: 10.0.0.0/16 - state: - description: The current state of the static route. - returned: always - type: str - sample: available - state: - description: The current state of the VPN connection. - returned: always - type: str - sample: available - tags: - description: Any tags assigned to the VPN connection. - returned: always - type: dict - sample: { - "Name": "test-conn" - } - type: - description: The type of VPN connection. - returned: always - type: str - sample: ipsec.1 - vgw_telemetry: - description: Information about the VPN tunnel. - returned: always - type: complex - contains: - accepted_route_count: - description: The number of accepted routes. - returned: always - type: int - sample: 0 - last_status_change: - description: The date and time of the last change in status. - returned: always - type: str - sample: "2018-02-09T14:35:27+00:00" - outside_ip_address: - description: The Internet-routable IP address of the virtual private gateway's outside interface. - returned: always - type: str - sample: 13.127.79.191 - status: - description: The status of the VPN tunnel. - returned: always - type: str - sample: DOWN - status_message: - description: If an error occurs, a description of the error. - returned: always - type: str - sample: IPSEC IS DOWN - certificate_arn: - description: The Amazon Resource Name of the virtual private gateway tunnel endpoint certificate. - returned: when a private certificate is used for authentication - type: str - sample: "arn:aws:acm:us-east-1:123456789101:certificate/c544d8ce-20b8-4fff-98b0-example" - vpn_connection_id: - description: The ID of the VPN connection. - returned: always - type: str - sample: vpn-f700d5c0 - vpn_gateway_id: - description: The ID of the virtual private gateway at the AWS side of the VPN connection. - returned: always - type: str - sample: vgw-cbe56bfb -''' - -import json -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict - - -def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj - - -def list_vpn_connections(connection, module): - params = dict() - - params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - params['VpnConnectionIds'] = module.params.get('vpn_connection_ids') - - try: - result = json.loads(json.dumps(connection.describe_vpn_connections(**params), default=date_handler)) - except ValueError as e: - module.fail_json_aws(e, msg="Cannot validate JSON data") - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not describe customer gateways") - snaked_vpn_connections = [camel_dict_to_snake_dict(vpn_connection) for vpn_connection in result['VpnConnections']] - if snaked_vpn_connections: - for vpn_connection in snaked_vpn_connections: - vpn_connection['tags'] = boto3_tag_list_to_ansible_dict(vpn_connection.get('tags', [])) - module.exit_json(changed=False, vpn_connections=snaked_vpn_connections) - - -def main(): - - argument_spec = dict( - vpn_connection_ids=dict(default=[], type='list'), - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - mutually_exclusive=[['vpn_connection_ids', 'filters']], - supports_check_mode=True) - if module._module._name == 'ec2_vpc_vpn_facts': - module._module.deprecate("The 'ec2_vpc_vpn_facts' module has been renamed to 'ec2_vpc_vpn_info'", version='2.13') - - connection = module.client('ec2') - - list_vpn_connections(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_win_password.py b/lib/ansible/modules/cloud/amazon/ec2_win_password.py deleted file mode 100644 index 4cb0c5003d..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_win_password.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_win_password -short_description: Gets the default administrator password for ec2 windows instances -description: - - Gets the default administrator password from any EC2 Windows instance. The instance is referenced by its id (e.g. C(i-XXXXXXX)). - - This module has a dependency on python-boto. -version_added: "2.0" -author: "Rick Mendes (@rickmendes)" -options: - instance_id: - description: - - The instance id to get the password data from. - required: true - type: str - key_file: - description: - - Path to the file containing the key pair used on the instance. - - Conflicts with I(key_data). - required: false - type: path - key_data: - version_added: "2.8" - description: - - The private key (usually stored in vault). - - Conflicts with I(key_file), - required: false - type: str - key_passphrase: - version_added: "2.0" - description: - - The passphrase for the instance key pair. The key must use DES or 3DES encryption for this module to decrypt it. You can use openssl to - convert your password protected keys if they do not use DES or 3DES. ex) C(openssl rsa -in current_key -out new_key -des3). - type: str - wait: - version_added: "2.0" - description: - - Whether or not to wait for the password to be available before returning. - type: bool - default: false - wait_timeout: - version_added: "2.0" - description: - - Number of seconds to wait before giving up. - default: 120 - type: int - -extends_documentation_fragment: - - aws - - ec2 - -requirements: - - cryptography - -notes: - - As of Ansible 2.4, this module requires the python cryptography module rather than the - older pycrypto module. -''' - -EXAMPLES = ''' -# Example of getting a password -- name: get the Administrator password - ec2_win_password: - profile: my-boto-profile - instance_id: i-XXXXXX - region: us-east-1 - key_file: "~/aws-creds/my_test_key.pem" - -# Example of getting a password using a variable -- name: get the Administrator password - ec2_win_password: - profile: my-boto-profile - instance_id: i-XXXXXX - region: us-east-1 - key_data: "{{ ec2_private_key }}" - -# Example of getting a password with a password protected key -- name: get the Administrator password - ec2_win_password: - profile: my-boto-profile - instance_id: i-XXXXXX - region: us-east-1 - key_file: "~/aws-creds/my_protected_test_key.pem" - key_passphrase: "secret" - -# Example of waiting for a password -- name: get the Administrator password - ec2_win_password: - profile: my-boto-profile - instance_id: i-XXXXXX - region: us-east-1 - key_file: "~/aws-creds/my_test_key.pem" - wait: yes - wait_timeout: 45 -''' - -import datetime -import time -from base64 import b64decode - -try: - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 - from cryptography.hazmat.primitives.serialization import load_pem_private_key - HAS_CRYPTOGRAPHY = True -except ImportError: - HAS_CRYPTOGRAPHY = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, ec2_argument_spec, ec2_connect -from ansible.module_utils._text import to_bytes - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - instance_id=dict(required=True), - key_file=dict(required=False, default=None, type='path'), - key_passphrase=dict(no_log=True, default=None, required=False), - key_data=dict(no_log=True, default=None, required=False), - wait=dict(type='bool', default=False, required=False), - wait_timeout=dict(default=120, required=False, type='int'), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='Boto required for this module.') - - if not HAS_CRYPTOGRAPHY: - module.fail_json(msg='cryptography package required for this module.') - - instance_id = module.params.get('instance_id') - key_file = module.params.get('key_file') - key_data = module.params.get('key_data') - if module.params.get('key_passphrase') is None: - b_key_passphrase = None - else: - b_key_passphrase = to_bytes(module.params.get('key_passphrase'), errors='surrogate_or_strict') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - ec2 = ec2_connect(module) - - if wait: - start = datetime.datetime.now() - end = start + datetime.timedelta(seconds=wait_timeout) - - while datetime.datetime.now() < end: - data = ec2.get_password_data(instance_id) - decoded = b64decode(data) - if not decoded: - time.sleep(5) - else: - break - else: - data = ec2.get_password_data(instance_id) - decoded = b64decode(data) - - if wait and datetime.datetime.now() >= end: - module.fail_json(msg="wait for password timeout after %d seconds" % wait_timeout) - - if key_file is not None and key_data is None: - try: - with open(key_file, 'rb') as f: - key = load_pem_private_key(f.read(), b_key_passphrase, default_backend()) - except IOError as e: - # Handle bad files - module.fail_json(msg="I/O error (%d) opening key file: %s" % (e.errno, e.strerror)) - except (ValueError, TypeError) as e: - # Handle issues loading key - module.fail_json(msg="unable to parse key file") - elif key_data is not None and key_file is None: - try: - key = load_pem_private_key(key_data, b_key_passphrase, default_backend()) - except (ValueError, TypeError) as e: - module.fail_json(msg="unable to parse key data") - - try: - decrypted = key.decrypt(decoded, PKCS1v15()) - except ValueError as e: - decrypted = None - - if decrypted is None: - module.exit_json(win_password='', changed=False) - else: - if wait: - elapsed = datetime.datetime.now() - start - module.exit_json(win_password=decrypted, changed=True, elapsed=elapsed.seconds) - else: - module.exit_json(win_password=decrypted, changed=True) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_attribute.py b/lib/ansible/modules/cloud/amazon/ecs_attribute.py deleted file mode 100644 index 3d5ec3646b..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_attribute.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ecs_attribute -short_description: manage ecs attributes -description: - - Create, update or delete ECS container instance attributes. -version_added: "2.4" -author: Andrej Svenke (@anryko) -requirements: [ botocore, boto3 ] -options: - cluster: - description: - - The short name or full Amazon Resource Name (ARN) of the cluster - that contains the resource to apply attributes. - required: true - type: str - state: - description: - - The desired state of the attributes. - required: false - default: present - choices: ['present', 'absent'] - type: str - attributes: - description: - - List of attributes. - required: true - type: list - elements: dict - suboptions: - name: - description: - - The name of the attribute. Up to 128 letters (uppercase and lowercase), - numbers, hyphens, underscores, and periods are allowed. - required: true - type: str - value: - description: - - The value of the attribute. Up to 128 letters (uppercase and lowercase), - numbers, hyphens, underscores, periods, at signs (@), forward slashes, colons, - and spaces are allowed. - required: false - type: str - ec2_instance_id: - description: - - EC2 instance ID of ECS cluster container instance. - required: true - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Set attributes -- ecs_attribute: - state: present - cluster: test-cluster - ec2_instance_id: "{{ ec2_id }}" - attributes: - - flavor: test - - migrated - delegate_to: localhost - -# Delete attributes -- ecs_attribute: - state: absent - cluster: test-cluster - ec2_instance_id: "{{ ec2_id }}" - attributes: - - flavor: test - - migrated - delegate_to: localhost -''' - -RETURN = ''' -attributes: - description: attributes - type: complex - returned: always - contains: - cluster: - description: cluster name - type: str - ec2_instance_id: - description: ec2 instance id of ecs container instance - type: str - attributes: - description: list of attributes - type: list - elements: dict - contains: - name: - description: name of the attribute - type: str - value: - description: value of the attribute - returned: if present - type: str -''' - -try: - import boto3 - from botocore.exceptions import ClientError, EndpointConnectionError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info - - -class EcsAttributes(object): - """Handles ECS Cluster Attribute""" - - def __init__(self, module, attributes): - self.module = module - self.attributes = attributes if self._validate_attrs(attributes) else self._parse_attrs(attributes) - - def __bool__(self): - return bool(self.attributes) - - __nonzero__ = __bool__ - - def __iter__(self): - return iter(self.attributes) - - @staticmethod - def _validate_attrs(attrs): - return all(tuple(attr.keys()) in (('name', 'value'), ('value', 'name')) for attr in attrs) - - def _parse_attrs(self, attrs): - attrs_parsed = [] - for attr in attrs: - if isinstance(attr, dict): - if len(attr) != 1: - self.module.fail_json(msg="Incorrect attribute format - %s" % str(attr)) - name, value = list(attr.items())[0] - attrs_parsed.append({'name': name, 'value': value}) - elif isinstance(attr, str): - attrs_parsed.append({'name': attr, 'value': None}) - else: - self.module.fail_json(msg="Incorrect attributes format - %s" % str(attrs)) - - return attrs_parsed - - def _setup_attr_obj(self, ecs_arn, name, value=None, skip_value=False): - attr_obj = {'targetType': 'container-instance', - 'targetId': ecs_arn, - 'name': name} - if not skip_value and value is not None: - attr_obj['value'] = value - - return attr_obj - - def get_for_ecs_arn(self, ecs_arn, skip_value=False): - """ - Returns list of attribute dicts ready to be passed to boto3 - attributes put/delete methods. - """ - return [self._setup_attr_obj(ecs_arn, skip_value=skip_value, **attr) for attr in self.attributes] - - def diff(self, attrs): - """ - Returns EcsAttributes Object containing attributes which are present - in self but are absent in passed attrs (EcsAttributes Object). - """ - attrs_diff = [attr for attr in self.attributes if attr not in attrs] - return EcsAttributes(self.module, attrs_diff) - - -class Ec2EcsInstance(object): - """Handle ECS Cluster Remote Operations""" - - def __init__(self, module, cluster, ec2_id): - self.module = module - self.cluster = cluster - self.ec2_id = ec2_id - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg=("Region must be specified as a parameter," - " in EC2_REGION or AWS_REGION environment" - " variables or in boto configuration file")) - self.ecs = boto3_conn(module, conn_type='client', resource='ecs', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - - self.ecs_arn = self._get_ecs_arn() - - def _get_ecs_arn(self): - try: - ecs_instances_arns = self.ecs.list_container_instances(cluster=self.cluster)['containerInstanceArns'] - ec2_instances = self.ecs.describe_container_instances(cluster=self.cluster, - containerInstances=ecs_instances_arns)['containerInstances'] - except (ClientError, EndpointConnectionError) as e: - self.module.fail_json(msg="Can't connect to the cluster - %s" % str(e)) - - try: - ecs_arn = next(inst for inst in ec2_instances - if inst['ec2InstanceId'] == self.ec2_id)['containerInstanceArn'] - except StopIteration: - self.module.fail_json(msg="EC2 instance Id not found in ECS cluster - %s" % str(self.cluster)) - - return ecs_arn - - def attrs_put(self, attrs): - """Puts attributes on ECS container instance""" - try: - self.ecs.put_attributes(cluster=self.cluster, - attributes=attrs.get_for_ecs_arn(self.ecs_arn)) - except ClientError as e: - self.module.fail_json(msg=str(e)) - - def attrs_delete(self, attrs): - """Deletes attributes from ECS container instance.""" - try: - self.ecs.delete_attributes(cluster=self.cluster, - attributes=attrs.get_for_ecs_arn(self.ecs_arn, skip_value=True)) - except ClientError as e: - self.module.fail_json(msg=str(e)) - - def attrs_get_by_name(self, attrs): - """ - Returns EcsAttributes object containing attributes from ECS container instance with names - matching to attrs.attributes (EcsAttributes Object). - """ - attr_objs = [{'targetType': 'container-instance', 'attributeName': attr['name']} - for attr in attrs] - - try: - matched_ecs_targets = [attr_found for attr_obj in attr_objs - for attr_found in self.ecs.list_attributes(cluster=self.cluster, **attr_obj)['attributes']] - except ClientError as e: - self.module.fail_json(msg="Can't connect to the cluster - %s" % str(e)) - - matched_objs = [target for target in matched_ecs_targets - if target['targetId'] == self.ecs_arn] - - results = [{'name': match['name'], 'value': match.get('value', None)} - for match in matched_objs] - - return EcsAttributes(self.module, results) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=False, default='present', choices=['present', 'absent']), - cluster=dict(required=True, type='str'), - ec2_instance_id=dict(required=True, type='str'), - attributes=dict(required=True, type='list'), - )) - - required_together = [['cluster', 'ec2_instance_id', 'attributes']] - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, - required_together=required_together) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - cluster = module.params['cluster'] - ec2_instance_id = module.params['ec2_instance_id'] - attributes = module.params['attributes'] - - conti = Ec2EcsInstance(module, cluster, ec2_instance_id) - attrs = EcsAttributes(module, attributes) - - results = {'changed': False, - 'attributes': [ - {'cluster': cluster, - 'ec2_instance_id': ec2_instance_id, - 'attributes': attributes} - ]} - - attrs_present = conti.attrs_get_by_name(attrs) - - if module.params['state'] == 'present': - attrs_diff = attrs.diff(attrs_present) - if not attrs_diff: - module.exit_json(**results) - - conti.attrs_put(attrs_diff) - results['changed'] = True - - elif module.params['state'] == 'absent': - if not attrs_present: - module.exit_json(**results) - - conti.attrs_delete(attrs_present) - results['changed'] = True - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_cluster.py b/lib/ansible/modules/cloud/amazon/ecs_cluster.py deleted file mode 100644 index 355e74551e..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_cluster.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_cluster -short_description: Create or terminate ECS clusters. -notes: - - When deleting a cluster, the information returned is the state of the cluster prior to deletion. - - It will also wait for a cluster to have instances registered to it. -description: - - Creates or terminates ecs clusters. -version_added: "2.0" -author: Mark Chance (@Java1Guy) -requirements: [ boto3 ] -options: - state: - description: - - The desired state of the cluster. - required: true - choices: ['present', 'absent', 'has_instances'] - type: str - name: - description: - - The cluster name. - required: true - type: str - delay: - description: - - Number of seconds to wait. - required: false - type: int - default: 10 - repeat: - description: - - The number of times to wait for the cluster to have an instance. - required: false - type: int - default: 10 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Cluster creation -- ecs_cluster: - name: default - state: present - -# Cluster deletion -- ecs_cluster: - name: default - state: absent - -- name: Wait for register - ecs_cluster: - name: "{{ new_cluster }}" - state: has_instances - delay: 10 - repeat: 10 - register: task_output - -''' -RETURN = ''' -activeServicesCount: - description: how many services are active in this cluster - returned: 0 if a new cluster - type: int -clusterArn: - description: the ARN of the cluster just created - type: str - returned: 0 if a new cluster - sample: arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster-mfshcdok -clusterName: - description: name of the cluster just created (should match the input argument) - type: str - returned: always - sample: test-cluster-mfshcdok -pendingTasksCount: - description: how many tasks are waiting to run in this cluster - returned: 0 if a new cluster - type: int -registeredContainerInstancesCount: - description: how many container instances are available in this cluster - returned: 0 if a new cluster - type: int -runningTasksCount: - description: how many tasks are running in this cluster - returned: 0 if a new cluster - type: int -status: - description: the status of the new cluster - returned: always - type: str - sample: ACTIVE -''' -import time - -try: - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info - - -class EcsClusterManager: - """Handles ECS Clusters""" - - def __init__(self, module): - self.module = module - - # self.ecs = boto3.client('ecs') - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - self.ecs = boto3_conn(module, conn_type='client', resource='ecs', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - - def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): - for c in array_of_clusters: - if c[field_name].endswith(cluster_name): - return c - return None - - def describe_cluster(self, cluster_name): - response = self.ecs.describe_clusters(clusters=[ - cluster_name - ]) - if len(response['failures']) > 0: - c = self.find_in_array(response['failures'], cluster_name, 'arn') - if c and c['reason'] == 'MISSING': - return None - # fall thru and look through found ones - if len(response['clusters']) > 0: - c = self.find_in_array(response['clusters'], cluster_name) - if c: - return c - raise Exception("Unknown problem describing cluster %s." % cluster_name) - - def create_cluster(self, clusterName='default'): - response = self.ecs.create_cluster(clusterName=clusterName) - return response['cluster'] - - def delete_cluster(self, clusterName): - return self.ecs.delete_cluster(cluster=clusterName) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent', 'has_instances']), - name=dict(required=True, type='str'), - delay=dict(required=False, type='int', default=10), - repeat=dict(required=False, type='int', default=10) - )) - required_together = [['state', 'name']] - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - cluster_mgr = EcsClusterManager(module) - try: - existing = cluster_mgr.describe_cluster(module.params['name']) - except Exception as e: - module.fail_json(msg="Exception describing cluster '" + module.params['name'] + "': " + str(e)) - - results = dict(changed=False) - if module.params['state'] == 'present': - if existing and 'status' in existing and existing['status'] == "ACTIVE": - results['cluster'] = existing - else: - if not module.check_mode: - # doesn't exist. create it. - results['cluster'] = cluster_mgr.create_cluster(module.params['name']) - results['changed'] = True - - # delete the cluster - elif module.params['state'] == 'absent': - if not existing: - pass - else: - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted - results['cluster'] = existing - if 'status' in existing and existing['status'] == "INACTIVE": - results['changed'] = False - else: - if not module.check_mode: - cluster_mgr.delete_cluster(module.params['name']) - results['changed'] = True - elif module.params['state'] == 'has_instances': - if not existing: - module.fail_json(msg="Cluster '" + module.params['name'] + " not found.") - return - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted - delay = module.params['delay'] - repeat = module.params['repeat'] - time.sleep(delay) - count = 0 - for i in range(repeat): - existing = cluster_mgr.describe_cluster(module.params['name']) - count = existing['registeredContainerInstancesCount'] - if count > 0: - results['changed'] = True - break - time.sleep(delay) - if count == 0 and i is repeat - 1: - module.fail_json(msg="Cluster instance count still zero after " + str(repeat) + " tries of " + str(delay) + " seconds each.") - return - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_ecr.py b/lib/ansible/modules/cloud/amazon/ecs_ecr.py deleted file mode 100644 index 3eb70c6d3c..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_ecr.py +++ /dev/null @@ -1,531 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -* - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ecs_ecr -version_added: "2.3" -short_description: Manage Elastic Container Registry repositories -description: - - Manage Elastic Container Registry repositories. -requirements: [ boto3 ] -options: - name: - description: - - The name of the repository. - required: true - type: str - registry_id: - description: - - AWS account id associated with the registry. - - If not specified, the default registry is assumed. - required: false - type: str - policy: - description: - - JSON or dict that represents the new policy. - required: false - type: json - force_set_policy: - description: - - If I(force_set_policy=false), it prevents setting a policy that would prevent you from - setting another policy in the future. - required: false - default: false - type: bool - purge_policy: - description: - - If yes, remove the policy from the repository. - - Alias C(delete_policy) has been deprecated and will be removed in Ansible 2.14 - required: false - default: false - type: bool - aliases: [ delete_policy ] - image_tag_mutability: - description: - - Configure whether repository should be mutable (ie. an already existing tag can be overwritten) or not. - required: false - choices: [mutable, immutable] - default: 'mutable' - version_added: '2.10' - type: str - lifecycle_policy: - description: - - JSON or dict that represents the new lifecycle policy - required: false - version_added: '2.10' - type: json - purge_lifecycle_policy: - description: - - if yes, remove the lifecycle policy from the repository - required: false - default: false - version_added: '2.10' - type: bool - state: - description: - - Create or destroy the repository. - required: false - choices: [present, absent] - default: 'present' - type: str -author: - - David M. Lee (@leedm777) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# If the repository does not exist, it is created. If it does exist, would not -# affect any policies already on it. -- name: ecr-repo - ecs_ecr: name=super/cool - -- name: destroy-ecr-repo - ecs_ecr: name=old/busted state=absent - -- name: Cross account ecr-repo - ecs_ecr: registry_id=999999999999 name=cross/account - -- name: set-policy as object - ecs_ecr: - name: needs-policy-object - policy: - Version: '2008-10-17' - Statement: - - Sid: read-only - Effect: Allow - Principal: - AWS: '{{ read_only_arn }}' - Action: - - ecr:GetDownloadUrlForLayer - - ecr:BatchGetImage - - ecr:BatchCheckLayerAvailability - -- name: set-policy as string - ecs_ecr: - name: needs-policy-string - policy: "{{ lookup('template', 'policy.json.j2') }}" - -- name: delete-policy - ecs_ecr: - name: needs-no-policy - purge_policy: yes - -- name: create immutable ecr-repo - ecs_ecr: - name: super/cool - image_tag_mutability: immutable - -- name: set-lifecycle-policy - ecs_ecr: - name: needs-lifecycle-policy - lifecycle_policy: - rules: - - rulePriority: 1 - description: new policy - selection: - tagStatus: untagged - countType: sinceImagePushed - countUnit: days - countNumber: 365 - action: - type: expire - -- name: purge-lifecycle-policy - ecs_ecr: - name: needs-no-lifecycle-policy - purge_lifecycle_policy: true -''' - -RETURN = ''' -state: - type: str - description: The asserted state of the repository (present, absent) - returned: always -created: - type: bool - description: If true, the repository was created - returned: always -name: - type: str - description: The name of the repository - returned: "when state == 'absent'" -repository: - type: dict - description: The created or updated repository - returned: "when state == 'present'" - sample: - createdAt: '2017-01-17T08:41:32-06:00' - registryId: '999999999999' - repositoryArn: arn:aws:ecr:us-east-1:999999999999:repository/ecr-test-1484664090 - repositoryName: ecr-test-1484664090 - repositoryUri: 999999999999.dkr.ecr.us-east-1.amazonaws.com/ecr-test-1484664090 -''' - -import json -import traceback - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import boto_exception, compare_policies, sort_json_policy_dict -from ansible.module_utils.six import string_types - - -def build_kwargs(registry_id): - """ - Builds a kwargs dict which may contain the optional registryId. - - :param registry_id: Optional string containing the registryId. - :return: kwargs dict with registryId, if given - """ - if not registry_id: - return dict() - else: - return dict(registryId=registry_id) - - -class EcsEcr: - def __init__(self, module): - self.ecr = module.client('ecr') - self.sts = module.client('sts') - self.check_mode = module.check_mode - self.changed = False - self.skipped = False - - def get_repository(self, registry_id, name): - try: - res = self.ecr.describe_repositories( - repositoryNames=[name], **build_kwargs(registry_id)) - repos = res.get('repositories') - return repos and repos[0] - except ClientError as err: - code = err.response['Error'].get('Code', 'Unknown') - if code == 'RepositoryNotFoundException': - return None - raise - - def get_repository_policy(self, registry_id, name): - try: - res = self.ecr.get_repository_policy( - repositoryName=name, **build_kwargs(registry_id)) - text = res.get('policyText') - return text and json.loads(text) - except ClientError as err: - code = err.response['Error'].get('Code', 'Unknown') - if code == 'RepositoryPolicyNotFoundException': - return None - raise - - def create_repository(self, registry_id, name, image_tag_mutability): - if registry_id: - default_registry_id = self.sts.get_caller_identity().get('Account') - if registry_id != default_registry_id: - raise Exception('Cannot create repository in registry {0}.' - 'Would be created in {1} instead.'.format(registry_id, default_registry_id)) - - if not self.check_mode: - repo = self.ecr.create_repository( - repositoryName=name, - imageTagMutability=image_tag_mutability).get('repository') - self.changed = True - return repo - else: - self.skipped = True - return dict(repositoryName=name) - - def set_repository_policy(self, registry_id, name, policy_text, force): - if not self.check_mode: - policy = self.ecr.set_repository_policy( - repositoryName=name, - policyText=policy_text, - force=force, - **build_kwargs(registry_id)) - self.changed = True - return policy - else: - self.skipped = True - if self.get_repository(registry_id, name) is None: - printable = name - if registry_id: - printable = '{0}:{1}'.format(registry_id, name) - raise Exception( - 'could not find repository {0}'.format(printable)) - return - - def delete_repository(self, registry_id, name): - if not self.check_mode: - repo = self.ecr.delete_repository( - repositoryName=name, **build_kwargs(registry_id)) - self.changed = True - return repo - else: - repo = self.get_repository(registry_id, name) - if repo: - self.skipped = True - return repo - return None - - def delete_repository_policy(self, registry_id, name): - if not self.check_mode: - policy = self.ecr.delete_repository_policy( - repositoryName=name, **build_kwargs(registry_id)) - self.changed = True - return policy - else: - policy = self.get_repository_policy(registry_id, name) - if policy: - self.skipped = True - return policy - return None - - def put_image_tag_mutability(self, registry_id, name, new_mutability_configuration): - repo = self.get_repository(registry_id, name) - current_mutability_configuration = repo.get('imageTagMutability') - - if current_mutability_configuration != new_mutability_configuration: - if not self.check_mode: - self.ecr.put_image_tag_mutability( - repositoryName=name, - imageTagMutability=new_mutability_configuration, - **build_kwargs(registry_id)) - else: - self.skipped = True - self.changed = True - - repo['imageTagMutability'] = new_mutability_configuration - return repo - - def get_lifecycle_policy(self, registry_id, name): - try: - res = self.ecr.get_lifecycle_policy( - repositoryName=name, **build_kwargs(registry_id)) - text = res.get('lifecyclePolicyText') - return text and json.loads(text) - except ClientError as err: - code = err.response['Error'].get('Code', 'Unknown') - if code == 'LifecyclePolicyNotFoundException': - return None - raise - - def put_lifecycle_policy(self, registry_id, name, policy_text): - if not self.check_mode: - policy = self.ecr.put_lifecycle_policy( - repositoryName=name, - lifecyclePolicyText=policy_text, - **build_kwargs(registry_id)) - self.changed = True - return policy - else: - self.skipped = True - if self.get_repository(registry_id, name) is None: - printable = name - if registry_id: - printable = '{0}:{1}'.format(registry_id, name) - raise Exception( - 'could not find repository {0}'.format(printable)) - return - - def purge_lifecycle_policy(self, registry_id, name): - if not self.check_mode: - policy = self.ecr.delete_lifecycle_policy( - repositoryName=name, **build_kwargs(registry_id)) - self.changed = True - return policy - else: - policy = self.get_lifecycle_policy(registry_id, name) - if policy: - self.skipped = True - return policy - return None - - -def sort_lists_of_strings(policy): - for statement_index in range(0, len(policy.get('Statement', []))): - for key in policy['Statement'][statement_index]: - value = policy['Statement'][statement_index][key] - if isinstance(value, list) and all(isinstance(item, string_types) for item in value): - policy['Statement'][statement_index][key] = sorted(value) - return policy - - -def run(ecr, params): - # type: (EcsEcr, dict, int) -> Tuple[bool, dict] - result = {} - try: - name = params['name'] - state = params['state'] - policy_text = params['policy'] - purge_policy = params['purge_policy'] - registry_id = params['registry_id'] - force_set_policy = params['force_set_policy'] - image_tag_mutability = params['image_tag_mutability'].upper() - lifecycle_policy_text = params['lifecycle_policy'] - purge_lifecycle_policy = params['purge_lifecycle_policy'] - - # Parse policies, if they are given - try: - policy = policy_text and json.loads(policy_text) - except ValueError: - result['policy'] = policy_text - result['msg'] = 'Could not parse policy' - return False, result - - try: - lifecycle_policy = \ - lifecycle_policy_text and json.loads(lifecycle_policy_text) - except ValueError: - result['lifecycle_policy'] = lifecycle_policy_text - result['msg'] = 'Could not parse lifecycle_policy' - return False, result - - result['state'] = state - result['created'] = False - - repo = ecr.get_repository(registry_id, name) - - if state == 'present': - result['created'] = False - - if not repo: - repo = ecr.create_repository(registry_id, name, image_tag_mutability) - result['changed'] = True - result['created'] = True - else: - repo = ecr.put_image_tag_mutability(registry_id, name, image_tag_mutability) - result['repository'] = repo - - if purge_lifecycle_policy: - original_lifecycle_policy = \ - ecr.get_lifecycle_policy(registry_id, name) - - result['lifecycle_policy'] = None - - if original_lifecycle_policy: - ecr.purge_lifecycle_policy(registry_id, name) - result['changed'] = True - - elif lifecycle_policy_text is not None: - try: - lifecycle_policy = sort_json_policy_dict(lifecycle_policy) - result['lifecycle_policy'] = lifecycle_policy - - original_lifecycle_policy = ecr.get_lifecycle_policy( - registry_id, name) - - if original_lifecycle_policy: - original_lifecycle_policy = sort_json_policy_dict( - original_lifecycle_policy) - - if original_lifecycle_policy != lifecycle_policy: - ecr.put_lifecycle_policy(registry_id, name, - lifecycle_policy_text) - result['changed'] = True - except Exception: - # Some failure w/ the policy. It's helpful to know what the - # policy is. - result['lifecycle_policy'] = lifecycle_policy_text - raise - - if purge_policy: - original_policy = ecr.get_repository_policy(registry_id, name) - - result['policy'] = None - - if original_policy: - ecr.delete_repository_policy(registry_id, name) - result['changed'] = True - - elif policy_text is not None: - try: - # Sort any lists containing only string types - policy = sort_lists_of_strings(policy) - - result['policy'] = policy - - original_policy = ecr.get_repository_policy( - registry_id, name) - if original_policy: - original_policy = sort_lists_of_strings(original_policy) - - if compare_policies(original_policy, policy): - ecr.set_repository_policy( - registry_id, name, policy_text, force_set_policy) - result['changed'] = True - except Exception: - # Some failure w/ the policy. It's helpful to know what the - # policy is. - result['policy'] = policy_text - raise - - elif state == 'absent': - result['name'] = name - if repo: - ecr.delete_repository(registry_id, name) - result['changed'] = True - - except Exception as err: - msg = str(err) - if isinstance(err, ClientError): - msg = boto_exception(err) - result['msg'] = msg - result['exception'] = traceback.format_exc() - return False, result - - if ecr.skipped: - result['skipped'] = True - - if ecr.changed: - result['changed'] = True - - return True, result - - -def main(): - argument_spec = dict( - name=dict(required=True), - registry_id=dict(required=False), - state=dict(required=False, choices=['present', 'absent'], - default='present'), - force_set_policy=dict(required=False, type='bool', default=False), - policy=dict(required=False, type='json'), - image_tag_mutability=dict(required=False, choices=['mutable', 'immutable'], - default='mutable'), - purge_policy=dict(required=False, type='bool', aliases=['delete_policy'], - deprecated_aliases=[dict(name='delete_policy', version='2.14')]), - lifecycle_policy=dict(required=False, type='json'), - purge_lifecycle_policy=dict(required=False, type='bool') - ) - mutually_exclusive = [ - ['policy', 'purge_policy'], - ['lifecycle_policy', 'purge_lifecycle_policy']] - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, mutually_exclusive=mutually_exclusive) - - ecr = EcsEcr(module) - passed, result = run(ecr, module.params) - - if passed: - module.exit_json(**result) - else: - module.fail_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_service.py b/lib/ansible/modules/cloud/amazon/ecs_service.py deleted file mode 100644 index 11e5bb3f38..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_service.py +++ /dev/null @@ -1,850 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_service -short_description: Create, terminate, start or stop a service in ECS -description: - - Creates or terminates ECS. services. -notes: - - The service role specified must be assumable. (i.e. have a trust relationship for the ecs service, ecs.amazonaws.com) - - For details of the parameters and returns see U(https://boto3.readthedocs.io/en/latest/reference/services/ecs.html). - - An IAM role must have been previously created. -version_added: "2.1" -author: - - "Mark Chance (@Java1Guy)" - - "Darek Kaczynski (@kaczynskid)" - - "Stephane Maarek (@simplesteph)" - - "Zac Blazic (@zacblazic)" - -requirements: [ json, botocore, boto3 ] -options: - state: - description: - - The desired state of the service. - required: true - choices: ["present", "absent", "deleting"] - type: str - name: - description: - - The name of the service. - required: true - type: str - cluster: - description: - - The name of the cluster in which the service exists. - required: false - type: str - task_definition: - description: - - The task definition the service will run. - - This parameter is required when I(state=present). - required: false - type: str - load_balancers: - description: - - The list of ELBs defined for this service. - required: false - type: list - elements: str - desired_count: - description: - - The count of how many instances of the service. - - This parameter is required when I(state=present). - required: false - type: int - client_token: - description: - - Unique, case-sensitive identifier you provide to ensure the idempotency of the request. Up to 32 ASCII characters are allowed. - required: false - type: str - role: - description: - - The name or full Amazon Resource Name (ARN) of the IAM role that allows your Amazon ECS container agent to make calls to your load balancer - on your behalf. - - This parameter is only required if you are using a load balancer with your service in a network mode other than C(awsvpc). - required: false - type: str - delay: - description: - - The time to wait before checking that the service is available. - required: false - default: 10 - type: int - repeat: - description: - - The number of times to check that the service is available. - required: false - default: 10 - type: int - force_new_deployment: - description: - - Force deployment of service even if there are no changes. - required: false - version_added: 2.8 - type: bool - deployment_configuration: - description: - - Optional parameters that control the deployment_configuration. - - Format is '{"maximum_percent":<integer>, "minimum_healthy_percent":<integer>} - required: false - version_added: 2.3 - type: dict - suboptions: - maximum_percent: - type: int - description: Upper limit on the number of tasks in a service that are allowed in the RUNNING or PENDING state during a deployment. - minimum_healthy_percent: - type: int - description: A lower limit on the number of tasks in a service that must remain in the RUNNING state during a deployment. - placement_constraints: - description: - - The placement constraints for the tasks in the service. - required: false - version_added: 2.4 - type: list - elements: dict - suboptions: - placement_strategy: - description: - - The placement strategy objects to use for tasks in your service. You can specify a maximum of 5 strategy rules per service. - required: false - version_added: 2.4 - type: list - elements: dict - suboptions: - type: - description: The type of placement strategy. - type: str - field: - description: The field to apply the placement strategy against. - type: str - network_configuration: - description: - - Network configuration of the service. Only applicable for task definitions created with I(network_mode=awsvpc). - - I(assign_public_ip) requires botocore >= 1.8.4 - type: dict - suboptions: - subnets: - description: - - A list of subnet IDs to associate with the task - version_added: 2.6 - type: list - elements: str - security_groups: - description: - - A list of security group names or group IDs to associate with the task - version_added: 2.6 - type: list - elements: str - assign_public_ip: - description: - - Whether the task's elastic network interface receives a public IP address. - - This option requires botocore >= 1.8.4. - type: bool - version_added: 2.7 - launch_type: - description: - - The launch type on which to run your service. - required: false - version_added: 2.7 - choices: ["EC2", "FARGATE"] - type: str - health_check_grace_period_seconds: - description: - - Seconds to wait before health checking the freshly added/updated services. - - This option requires botocore >= 1.8.20. - required: false - version_added: 2.8 - type: int - service_registries: - description: - - Describes service discovery registries this service will register with. - type: list - elements: dict - required: false - version_added: 2.8 - suboptions: - container_name: - description: - - container name for service discovery registration - type: str - container_port: - description: - - container port for service discovery registration - type: int - arn: - description: - - Service discovery registry ARN - type: str - scheduling_strategy: - description: - - The scheduling strategy, defaults to "REPLICA" if not given to preserve previous behavior - required: false - version_added: 2.8 - choices: ["DAEMON", "REPLICA"] - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic provisioning example -- ecs_service: - state: present - name: console-test-service - cluster: new_cluster - task_definition: 'new_cluster-task:1' - desired_count: 0 - -- name: create ECS service on VPC network - ecs_service: - state: present - name: console-test-service - cluster: new_cluster - task_definition: 'new_cluster-task:1' - desired_count: 0 - network_configuration: - subnets: - - subnet-abcd1234 - security_groups: - - sg-aaaa1111 - - my_security_group - -# Simple example to delete -- ecs_service: - name: default - state: absent - cluster: new_cluster - -# With custom deployment configuration (added in version 2.3), placement constraints and strategy (added in version 2.4) -- ecs_service: - state: present - name: test-service - cluster: test-cluster - task_definition: test-task-definition - desired_count: 3 - deployment_configuration: - minimum_healthy_percent: 75 - maximum_percent: 150 - placement_constraints: - - type: memberOf - expression: 'attribute:flavor==test' - placement_strategy: - - type: binpack - field: memory -''' - -RETURN = ''' -service: - description: Details of created service. - returned: when creating a service - type: complex - contains: - clusterArn: - description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. - returned: always - type: str - desiredCount: - description: The desired number of instantiations of the task definition to keep running on the service. - returned: always - type: int - loadBalancers: - description: A list of load balancer objects - returned: always - type: complex - contains: - loadBalancerName: - description: the name - returned: always - type: str - containerName: - description: The name of the container to associate with the load balancer. - returned: always - type: str - containerPort: - description: The port on the container to associate with the load balancer. - returned: always - type: int - pendingCount: - description: The number of tasks in the cluster that are in the PENDING state. - returned: always - type: int - runningCount: - description: The number of tasks in the cluster that are in the RUNNING state. - returned: always - type: int - serviceArn: - description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region - of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, - arn:aws:ecs:region :012345678910 :service/my-service . - returned: always - type: str - serviceName: - description: A user-generated string used to identify the service - returned: always - type: str - status: - description: The valid values are ACTIVE, DRAINING, or INACTIVE. - returned: always - type: str - taskDefinition: - description: The ARN of a task definition to use for tasks in the service. - returned: always - type: str - deployments: - description: list of service deployments - returned: always - type: list - elements: dict - deploymentConfiguration: - description: dictionary of deploymentConfiguration - returned: always - type: complex - contains: - maximumPercent: - description: maximumPercent param - returned: always - type: int - minimumHealthyPercent: - description: minimumHealthyPercent param - returned: always - type: int - events: - description: list of service events - returned: always - type: list - elements: dict - placementConstraints: - description: List of placement constraints objects - returned: always - type: list - elements: dict - contains: - type: - description: The type of constraint. Valid values are distinctInstance and memberOf. - returned: always - type: str - expression: - description: A cluster query language expression to apply to the constraint. Note you cannot specify an expression if the constraint type is - distinctInstance. - returned: always - type: str - placementStrategy: - description: List of placement strategy objects - returned: always - type: list - elements: dict - contains: - type: - description: The type of placement strategy. Valid values are random, spread and binpack. - returned: always - type: str - field: - description: The field to apply the placement strategy against. For the spread placement strategy, valid values are instanceId - (or host, which has the same effect), or any platform or custom attribute that is applied to a container instance, - such as attribute:ecs.availability-zone. For the binpack placement strategy, valid values are CPU and MEMORY. - returned: always - type: str - -ansible_facts: - description: Facts about deleted service. - returned: when deleting a service - type: complex - contains: - service: - description: Details of deleted service. - returned: when service existed and was deleted - type: complex - contains: - clusterArn: - description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. - returned: always - type: str - desiredCount: - description: The desired number of instantiations of the task definition to keep running on the service. - returned: always - type: int - loadBalancers: - description: A list of load balancer objects - returned: always - type: complex - contains: - loadBalancerName: - description: the name - returned: always - type: str - containerName: - description: The name of the container to associate with the load balancer. - returned: always - type: str - containerPort: - description: The port on the container to associate with the load balancer. - returned: always - type: int - pendingCount: - description: The number of tasks in the cluster that are in the PENDING state. - returned: always - type: int - runningCount: - description: The number of tasks in the cluster that are in the RUNNING state. - returned: always - type: int - serviceArn: - description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region - of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, - arn:aws:ecs:region :012345678910 :service/my-service . - returned: always - type: str - serviceName: - description: A user-generated string used to identify the service - returned: always - type: str - status: - description: The valid values are ACTIVE, DRAINING, or INACTIVE. - returned: always - type: str - taskDefinition: - description: The ARN of a task definition to use for tasks in the service. - returned: always - type: str - deployments: - description: list of service deployments - returned: always - type: list - elements: dict - deploymentConfiguration: - description: dictionary of deploymentConfiguration - returned: always - type: complex - contains: - maximumPercent: - description: maximumPercent param - returned: always - type: int - minimumHealthyPercent: - description: minimumHealthyPercent param - returned: always - type: int - events: - description: list of service events - returned: always - type: list - elements: dict - placementConstraints: - description: List of placement constraints objects - returned: always - type: list - elements: dict - contains: - type: - description: The type of constraint. Valid values are distinctInstance and memberOf. - returned: always - type: str - expression: - description: A cluster query language expression to apply to the constraint. Note you cannot specify an expression if - the constraint type is distinctInstance. - returned: always - type: str - placementStrategy: - description: List of placement strategy objects - returned: always - type: list - elements: dict - contains: - type: - description: The type of placement strategy. Valid values are random, spread and binpack. - returned: always - type: str - field: - description: The field to apply the placement strategy against. For the spread placement strategy, valid values are instanceId - (or host, which has the same effect), or any platform or custom attribute that is applied to a container instance, - such as attribute:ecs.availability-zone. For the binpack placement strategy, valid values are CPU and MEMORY. - returned: always - type: str -''' -import time - -DEPLOYMENT_CONFIGURATION_TYPE_MAP = { - 'maximum_percent': 'int', - 'minimum_healthy_percent': 'int' -} - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import snake_dict_to_camel_dict, map_complex_type, get_ec2_security_group_ids_from_names - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -class EcsServiceManager: - """Handles ECS Services""" - - def __init__(self, module): - self.module = module - self.ecs = module.client('ecs') - self.ec2 = module.client('ec2') - - def format_network_configuration(self, network_config): - result = dict() - if network_config['subnets'] is not None: - result['subnets'] = network_config['subnets'] - else: - self.module.fail_json(msg="Network configuration must include subnets") - if network_config['security_groups'] is not None: - groups = network_config['security_groups'] - if any(not sg.startswith('sg-') for sg in groups): - try: - vpc_id = self.ec2.describe_subnets(SubnetIds=[result['subnets'][0]])['Subnets'][0]['VpcId'] - groups = get_ec2_security_group_ids_from_names(groups, self.ec2, vpc_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't look up security groups") - result['securityGroups'] = groups - if network_config['assign_public_ip'] is not None: - if self.module.botocore_at_least('1.8.4'): - if network_config['assign_public_ip'] is True: - result['assignPublicIp'] = "ENABLED" - else: - result['assignPublicIp'] = "DISABLED" - else: - self.module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use assign_public_ip in network_configuration') - return dict(awsvpcConfiguration=result) - - def find_in_array(self, array_of_services, service_name, field_name='serviceArn'): - for c in array_of_services: - if c[field_name].endswith(service_name): - return c - return None - - def describe_service(self, cluster_name, service_name): - response = self.ecs.describe_services( - cluster=cluster_name, - services=[service_name]) - msg = '' - if len(response['failures']) > 0: - c = self.find_in_array(response['failures'], service_name, 'arn') - msg += ", failure reason is " + c['reason'] - if c and c['reason'] == 'MISSING': - return None - # fall thru and look through found ones - if len(response['services']) > 0: - c = self.find_in_array(response['services'], service_name) - if c: - return c - raise Exception("Unknown problem describing service %s." % service_name) - - def is_matching_service(self, expected, existing): - if expected['task_definition'] != existing['taskDefinition']: - return False - - if (expected['load_balancers'] or []) != existing['loadBalancers']: - return False - - # expected is params. DAEMON scheduling strategy returns desired count equal to - # number of instances running; don't check desired count if scheduling strat is daemon - if (expected['scheduling_strategy'] != 'DAEMON'): - if (expected['desired_count'] or 0) != existing['desiredCount']: - return False - - return True - - def create_service(self, service_name, cluster_name, task_definition, load_balancers, - desired_count, client_token, role, deployment_configuration, - placement_constraints, placement_strategy, health_check_grace_period_seconds, - network_configuration, service_registries, launch_type, scheduling_strategy): - - params = dict( - cluster=cluster_name, - serviceName=service_name, - taskDefinition=task_definition, - loadBalancers=load_balancers, - clientToken=client_token, - role=role, - deploymentConfiguration=deployment_configuration, - placementConstraints=placement_constraints, - placementStrategy=placement_strategy - ) - if network_configuration: - params['networkConfiguration'] = network_configuration - if launch_type: - params['launchType'] = launch_type - if self.health_check_setable(params) and health_check_grace_period_seconds is not None: - params['healthCheckGracePeriodSeconds'] = health_check_grace_period_seconds - if service_registries: - params['serviceRegistries'] = service_registries - # desired count is not required if scheduling strategy is daemon - if desired_count is not None: - params['desiredCount'] = desired_count - - if scheduling_strategy: - params['schedulingStrategy'] = scheduling_strategy - response = self.ecs.create_service(**params) - return self.jsonize(response['service']) - - def update_service(self, service_name, cluster_name, task_definition, - desired_count, deployment_configuration, network_configuration, - health_check_grace_period_seconds, force_new_deployment): - params = dict( - cluster=cluster_name, - service=service_name, - taskDefinition=task_definition, - deploymentConfiguration=deployment_configuration) - if network_configuration: - params['networkConfiguration'] = network_configuration - if force_new_deployment: - params['forceNewDeployment'] = force_new_deployment - if health_check_grace_period_seconds is not None: - params['healthCheckGracePeriodSeconds'] = health_check_grace_period_seconds - # desired count is not required if scheduling strategy is daemon - if desired_count is not None: - params['desiredCount'] = desired_count - - response = self.ecs.update_service(**params) - return self.jsonize(response['service']) - - def jsonize(self, service): - # some fields are datetime which is not JSON serializable - # make them strings - if 'createdAt' in service: - service['createdAt'] = str(service['createdAt']) - if 'deployments' in service: - for d in service['deployments']: - if 'createdAt' in d: - d['createdAt'] = str(d['createdAt']) - if 'updatedAt' in d: - d['updatedAt'] = str(d['updatedAt']) - if 'events' in service: - for e in service['events']: - if 'createdAt' in e: - e['createdAt'] = str(e['createdAt']) - return service - - def delete_service(self, service, cluster=None): - return self.ecs.delete_service(cluster=cluster, service=service) - - def ecs_api_handles_network_configuration(self): - # There doesn't seem to be a nice way to inspect botocore to look - # for attributes (and networkConfiguration is not an explicit argument - # to e.g. ecs.run_task, it's just passed as a keyword argument) - return self.module.botocore_at_least('1.7.44') - - def health_check_setable(self, params): - load_balancers = params.get('loadBalancers', []) - # check if botocore (and thus boto3) is new enough for using the healthCheckGracePeriodSeconds parameter - return len(load_balancers) > 0 and self.module.botocore_at_least('1.8.20') - - -def main(): - argument_spec = dict( - state=dict(required=True, choices=['present', 'absent', 'deleting']), - name=dict(required=True, type='str'), - cluster=dict(required=False, type='str'), - task_definition=dict(required=False, type='str'), - load_balancers=dict(required=False, default=[], type='list'), - desired_count=dict(required=False, type='int'), - client_token=dict(required=False, default='', type='str'), - role=dict(required=False, default='', type='str'), - delay=dict(required=False, type='int', default=10), - repeat=dict(required=False, type='int', default=10), - force_new_deployment=dict(required=False, default=False, type='bool'), - deployment_configuration=dict(required=False, default={}, type='dict'), - placement_constraints=dict(required=False, default=[], type='list'), - placement_strategy=dict(required=False, default=[], type='list'), - health_check_grace_period_seconds=dict(required=False, type='int'), - network_configuration=dict(required=False, type='dict', options=dict( - subnets=dict(type='list'), - security_groups=dict(type='list'), - assign_public_ip=dict(type='bool') - )), - launch_type=dict(required=False, choices=['EC2', 'FARGATE']), - service_registries=dict(required=False, type='list', default=[]), - scheduling_strategy=dict(required=False, choices=['DAEMON', 'REPLICA']) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True, - required_if=[('state', 'present', ['task_definition']), - ('launch_type', 'FARGATE', ['network_configuration'])], - required_together=[['load_balancers', 'role']]) - - if module.params['state'] == 'present' and module.params['scheduling_strategy'] == 'REPLICA': - if module.params['desired_count'] is None: - module.fail_json(msg='state is present, scheduling_strategy is REPLICA; missing desired_count') - - service_mgr = EcsServiceManager(module) - if module.params['network_configuration']: - if not service_mgr.ecs_api_handles_network_configuration(): - module.fail_json(msg='botocore needs to be version 1.7.44 or higher to use network configuration') - network_configuration = service_mgr.format_network_configuration(module.params['network_configuration']) - else: - network_configuration = None - - deployment_configuration = map_complex_type(module.params['deployment_configuration'], - DEPLOYMENT_CONFIGURATION_TYPE_MAP) - - deploymentConfiguration = snake_dict_to_camel_dict(deployment_configuration) - serviceRegistries = list(map(snake_dict_to_camel_dict, module.params['service_registries'])) - - try: - existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) - except Exception as e: - module.fail_json(msg="Exception describing service '" + module.params['name'] + "' in cluster '" + module.params['cluster'] + "': " + str(e)) - - results = dict(changed=False) - - if module.params['launch_type']: - if not module.botocore_at_least('1.8.4'): - module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use launch_type') - if module.params['force_new_deployment']: - if not module.botocore_at_least('1.8.4'): - module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use force_new_deployment') - if module.params['health_check_grace_period_seconds']: - if not module.botocore_at_least('1.8.20'): - module.fail_json(msg='botocore needs to be version 1.8.20 or higher to use health_check_grace_period_seconds') - - if module.params['state'] == 'present': - - matching = False - update = False - - if existing and 'status' in existing and existing['status'] == "ACTIVE": - if module.params['force_new_deployment']: - update = True - elif service_mgr.is_matching_service(module.params, existing): - matching = True - results['service'] = existing - else: - update = True - - if not matching: - if not module.check_mode: - - role = module.params['role'] - clientToken = module.params['client_token'] - - loadBalancers = [] - for loadBalancer in module.params['load_balancers']: - if 'containerPort' in loadBalancer: - loadBalancer['containerPort'] = int(loadBalancer['containerPort']) - loadBalancers.append(loadBalancer) - - for loadBalancer in loadBalancers: - if 'containerPort' in loadBalancer: - loadBalancer['containerPort'] = int(loadBalancer['containerPort']) - - if update: - # check various parameters and boto versions and give a helpful error in boto is not new enough for feature - - if module.params['scheduling_strategy']: - if not module.botocore_at_least('1.10.37'): - module.fail_json(msg='botocore needs to be version 1.10.37 or higher to use scheduling_strategy') - elif (existing['schedulingStrategy']) != module.params['scheduling_strategy']: - module.fail_json(msg="It is not possible to update the scheduling strategy of an existing service") - - if module.params['service_registries']: - if not module.botocore_at_least('1.9.15'): - module.fail_json(msg='botocore needs to be version 1.9.15 or higher to use service_registries') - elif (existing['serviceRegistries'] or []) != serviceRegistries: - module.fail_json(msg="It is not possible to update the service registries of an existing service") - - if (existing['loadBalancers'] or []) != loadBalancers: - module.fail_json(msg="It is not possible to update the load balancers of an existing service") - - # update required - response = service_mgr.update_service(module.params['name'], - module.params['cluster'], - module.params['task_definition'], - module.params['desired_count'], - deploymentConfiguration, - network_configuration, - module.params['health_check_grace_period_seconds'], - module.params['force_new_deployment']) - - else: - try: - response = service_mgr.create_service(module.params['name'], - module.params['cluster'], - module.params['task_definition'], - loadBalancers, - module.params['desired_count'], - clientToken, - role, - deploymentConfiguration, - module.params['placement_constraints'], - module.params['placement_strategy'], - module.params['health_check_grace_period_seconds'], - network_configuration, - serviceRegistries, - module.params['launch_type'], - module.params['scheduling_strategy'] - ) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, msg="Couldn't create service") - - results['service'] = response - - results['changed'] = True - - elif module.params['state'] == 'absent': - if not existing: - pass - else: - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted - del existing['deployments'] - del existing['events'] - results['ansible_facts'] = existing - if 'status' in existing and existing['status'] == "INACTIVE": - results['changed'] = False - else: - if not module.check_mode: - try: - service_mgr.delete_service( - module.params['name'], - module.params['cluster'] - ) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, msg="Couldn't delete service") - results['changed'] = True - - elif module.params['state'] == 'deleting': - if not existing: - module.fail_json(msg="Service '" + module.params['name'] + " not found.") - return - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted - delay = module.params['delay'] - repeat = module.params['repeat'] - time.sleep(delay) - for i in range(repeat): - existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) - status = existing['status'] - if status == "INACTIVE": - results['changed'] = True - break - time.sleep(delay) - if i is repeat - 1: - module.fail_json(msg="Service still not deleted after " + str(repeat) + " tries of " + str(delay) + " seconds each.") - return - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_service_info.py b/lib/ansible/modules/cloud/amazon/ecs_service_info.py deleted file mode 100644 index 4a7b99c690..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_service_info.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_service_info -short_description: List or describe services in ECS -description: - - Lists or describes services in ECS. - - This module was called C(ecs_service_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(ecs_service_info) module no longer returns C(ansible_facts)! -version_added: "2.1" -author: - - "Mark Chance (@Java1Guy)" - - "Darek Kaczynski (@kaczynskid)" -requirements: [ json, botocore, boto3 ] -options: - details: - description: - - Set this to true if you want detailed information about the services. - required: false - default: false - type: bool - events: - description: - - Whether to return ECS service events. Only has an effect if I(details=true). - required: false - default: true - type: bool - version_added: "2.6" - cluster: - description: - - The cluster ARNS in which to list the services. - required: false - type: str - service: - description: - - One or more services to get details for - required: false - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic listing example -- ecs_service_info: - cluster: test-cluster - service: console-test-service - details: true - register: output - -# Basic listing example -- ecs_service_info: - cluster: test-cluster - register: output -''' - -RETURN = ''' -services: - description: When details is false, returns an array of service ARNs, otherwise an array of complex objects as described below. - returned: success - type: complex - contains: - clusterArn: - description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. - returned: always - type: str - desiredCount: - description: The desired number of instantiations of the task definition to keep running on the service. - returned: always - type: int - loadBalancers: - description: A list of load balancer objects - returned: always - type: complex - contains: - loadBalancerName: - description: the name - returned: always - type: str - containerName: - description: The name of the container to associate with the load balancer. - returned: always - type: str - containerPort: - description: The port on the container to associate with the load balancer. - returned: always - type: int - pendingCount: - description: The number of tasks in the cluster that are in the PENDING state. - returned: always - type: int - runningCount: - description: The number of tasks in the cluster that are in the RUNNING state. - returned: always - type: int - serviceArn: - description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . - returned: always - type: str - serviceName: - description: A user-generated string used to identify the service - returned: always - type: str - status: - description: The valid values are ACTIVE, DRAINING, or INACTIVE. - returned: always - type: str - taskDefinition: - description: The ARN of a task definition to use for tasks in the service. - returned: always - type: str - deployments: - description: list of service deployments - returned: always - type: list - elements: dict - events: - description: list of service events - returned: when events is true - type: list - elements: dict -''' # NOQA - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry - - -class EcsServiceManager: - """Handles ECS Services""" - - def __init__(self, module): - self.module = module - self.ecs = module.client('ecs') - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) - def list_services_with_backoff(self, **kwargs): - paginator = self.ecs.get_paginator('list_services') - try: - return paginator.paginate(**kwargs).build_full_result() - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'ClusterNotFoundException': - self.module.fail_json_aws(e, "Could not find cluster to list services") - else: - raise - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) - def describe_services_with_backoff(self, **kwargs): - return self.ecs.describe_services(**kwargs) - - def list_services(self, cluster): - fn_args = dict() - if cluster and cluster is not None: - fn_args['cluster'] = cluster - try: - response = self.list_services_with_backoff(**fn_args) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't list ECS services") - relevant_response = dict(services=response['serviceArns']) - return relevant_response - - def describe_services(self, cluster, services): - fn_args = dict() - if cluster and cluster is not None: - fn_args['cluster'] = cluster - fn_args['services'] = services - try: - response = self.describe_services_with_backoff(**fn_args) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't describe ECS services") - running_services = [self.extract_service_from(service) for service in response.get('services', [])] - services_not_running = response.get('failures', []) - return running_services, services_not_running - - def extract_service_from(self, service): - # some fields are datetime which is not JSON serializable - # make them strings - if 'deployments' in service: - for d in service['deployments']: - if 'createdAt' in d: - d['createdAt'] = str(d['createdAt']) - if 'updatedAt' in d: - d['updatedAt'] = str(d['updatedAt']) - if 'events' in service: - if not self.module.params['events']: - del service['events'] - else: - for e in service['events']: - if 'createdAt' in e: - e['createdAt'] = str(e['createdAt']) - return service - - -def chunks(l, n): - """Yield successive n-sized chunks from l.""" - """ https://stackoverflow.com/a/312464 """ - for i in range(0, len(l), n): - yield l[i:i + n] - - -def main(): - - argument_spec = dict( - details=dict(type='bool', default=False), - events=dict(type='bool', default=True), - cluster=dict(), - service=dict(type='list') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - is_old_facts = module._name == 'ecs_service_facts' - if is_old_facts: - module.deprecate("The 'ecs_service_facts' module has been renamed to 'ecs_service_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - show_details = module.params.get('details') - - task_mgr = EcsServiceManager(module) - if show_details: - if module.params['service']: - services = module.params['service'] - else: - services = task_mgr.list_services(module.params['cluster'])['services'] - ecs_info = dict(services=[], services_not_running=[]) - for chunk in chunks(services, 10): - running_services, services_not_running = task_mgr.describe_services(module.params['cluster'], chunk) - ecs_info['services'].extend(running_services) - ecs_info['services_not_running'].extend(services_not_running) - else: - ecs_info = task_mgr.list_services(module.params['cluster']) - - if is_old_facts: - module.exit_json(changed=False, ansible_facts=ecs_info, **ecs_info) - else: - module.exit_json(changed=False, **ecs_info) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_tag.py b/lib/ansible/modules/cloud/amazon/ecs_tag.py deleted file mode 100644 index 0a2b639233..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_tag.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2019, Michael Pechner <mikey@mikey.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, print_function -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: ecs_tag -short_description: create and remove tags on Amazon ECS resources -notes: - - none -description: - - Creates and removes tags for Amazon ECS resources. - - Resources are referenced by their cluster name. -version_added: '2.10' -author: - - Michael Pechner (@mpechner) -requirements: [ boto3, botocore ] -options: - cluster_name: - description: - - The name of the cluster whose resources we are tagging. - required: true - type: str - resource: - description: - - The ECS resource name. - - Required unless I(resource_type=cluster). - type: str - resource_type: - description: - - The type of resource. - default: cluster - choices: ['cluster', 'task', 'service', 'task_definition', 'container'] - type: str - state: - description: - - Whether the tags should be present or absent on the resource. - default: present - choices: ['present', 'absent'] - type: str - tags: - description: - - A dictionary of tags to add or remove from the resource. - - If the value provided for a tag is null and I(state=absent), the tag will be removed regardless of its current value. - type: dict - purge_tags: - description: - - Whether unspecified tags should be removed from the resource. - - Note that when combined with I(state=absent), specified tags with non-matching values are not purged. - type: bool - default: false -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = r''' -- name: Ensure tags are present on a resource - ecs_tag: - cluster_name: mycluster - resource_type: cluster - state: present - tags: - Name: ubervol - env: prod - -- name: Remove the Env tag - ecs_tag: - cluster_name: mycluster - resource_type: cluster - tags: - Env: - state: absent - -- name: Remove the Env tag if it's currently 'development' - ecs_tag: - cluster_name: mycluster - resource_type: cluster - tags: - Env: development - state: absent - -- name: Remove all tags except for Name from a cluster - ecs_tag: - cluster_name: mycluster - resource_type: cluster - tags: - Name: foo - state: absent - purge_tags: true -''' - -RETURN = r''' -tags: - description: A dict containing the tags on the resource - returned: always - type: dict -added_tags: - description: A dict of tags that were added to the resource - returned: If tags were added - type: dict -removed_tags: - description: A dict of tags that were removed from the resource - returned: If tags were removed - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, compare_aws_tags - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # Handled by AnsibleAWSModule -__metaclass__ = type - - -def get_tags(ecs, module, resource): - try: - return boto3_tag_list_to_ansible_dict(ecs.list_tags_for_resource(resourceArn=resource)['tags']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource)) - - -def get_arn(ecs, module, cluster_name, resource_type, resource): - - try: - if resource_type == 'cluster': - description = ecs.describe_clusters(clusters=[resource]) - resource_arn = description['clusters'][0]['clusterArn'] - elif resource_type == 'task': - description = ecs.describe_tasks(cluster=cluster_name, tasks=[resource]) - resource_arn = description['tasks'][0]['taskArn'] - elif resource_type == 'service': - description = ecs.describe_services(cluster=cluster_name, services=[resource]) - resource_arn = description['services'][0]['serviceArn'] - elif resource_type == 'task_definition': - description = ecs.describe_task_definition(taskDefinition=resource) - resource_arn = description['taskDefinition']['taskDefinitionArn'] - elif resource_type == 'container': - description = ecs.describe_container_instances(clusters=[resource]) - resource_arn = description['containerInstances'][0]['containerInstanceArn'] - except (IndexError, KeyError): - module.fail_json(msg='Failed to find {0} {1}'.format(resource_type, resource)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to find {0} {1}'.format(resource_type, resource)) - - return resource_arn - - -def main(): - argument_spec = dict( - cluster_name=dict(required=True), - resource=dict(required=False), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=False), - state=dict(default='present', choices=['present', 'absent']), - resource_type=dict(default='cluster', choices=['cluster', 'task', 'service', 'task_definition', 'container']) - ) - required_if = [('state', 'present', ['tags']), ('state', 'absent', ['tags'])] - - module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) - - resource_type = module.params['resource_type'] - cluster_name = module.params['cluster_name'] - if resource_type == 'cluster': - resource = cluster_name - else: - resource = module.params['resource'] - tags = module.params['tags'] - state = module.params['state'] - purge_tags = module.params['purge_tags'] - - result = {'changed': False} - - ecs = module.client('ecs') - - resource_arn = get_arn(ecs, module, cluster_name, resource_type, resource) - - current_tags = get_tags(ecs, module, resource_arn) - - add_tags, remove = compare_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: - ecs.untag_resource(resourceArn=resource_arn, tagKeys=list(remove_tags.keys())) - except (BotoCoreError, ClientError) as e: - module.fail_json_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: - tags = ansible_dict_to_boto3_tag_list(add_tags, tag_name_key_name='key', tag_value_key_name='value') - ecs.tag_resource(resourceArn=resource_arn, tags=tags) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set tags {0} on resource {1}'.format(add_tags, resource)) - - result['tags'] = get_tags(ecs, module, resource_arn) - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_task.py b/lib/ansible/modules/cloud/amazon/ecs_task.py deleted file mode 100644 index a0060ce9d3..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_task.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_task -short_description: Run, start or stop a task in ecs -description: - - Creates or deletes instances of task definitions. -version_added: "2.0" -author: Mark Chance (@Java1Guy) -requirements: [ json, botocore, boto3 ] -options: - operation: - description: - - Which task operation to execute. - required: True - choices: ['run', 'start', 'stop'] - type: str - cluster: - description: - - The name of the cluster to run the task on. - required: False - type: str - task_definition: - description: - - The task definition to start or run. - required: False - type: str - overrides: - description: - - A dictionary of values to pass to the new instances. - required: False - type: dict - count: - description: - - How many new instances to start. - required: False - type: int - task: - description: - - The task to stop. - required: False - type: str - container_instances: - description: - - The list of container instances on which to deploy the task. - required: False - type: list - elements: str - started_by: - description: - - A value showing who or what started the task (for informational purposes). - required: False - type: str - network_configuration: - description: - - Network configuration of the service. Only applicable for task definitions created with I(network_mode=awsvpc). - type: dict - suboptions: - subnets: - description: A list of subnet IDs to which the task is attached. - type: list - elements: str - security_groups: - description: A list of group names or group IDs for the task. - type: list - elements: str - version_added: 2.6 - launch_type: - description: - - The launch type on which to run your service. - required: false - version_added: 2.8 - choices: ["EC2", "FARGATE"] - type: str - tags: - type: dict - description: - - Tags that will be added to ecs tasks on start and run - required: false - version_added: "2.10" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple example of run task -- name: Run task - ecs_task: - operation: run - cluster: console-sample-app-static-cluster - task_definition: console-sample-app-static-taskdef - count: 1 - started_by: ansible_user - register: task_output - -# Simple example of start task - -- name: Start a task - ecs_task: - operation: start - cluster: console-sample-app-static-cluster - task_definition: console-sample-app-static-taskdef - task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" - tags: - resourceName: a_task_for_ansible_to_run - type: long_running_task - network: internal - version: 1.4 - container_instances: - - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 - started_by: ansible_user - network_configuration: - subnets: - - subnet-abcd1234 - security_groups: - - sg-aaaa1111 - - my_security_group - register: task_output - -- name: RUN a task on Fargate - ecs_task: - operation: run - cluster: console-sample-app-static-cluster - task_definition: console-sample-app-static-taskdef - task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" - started_by: ansible_user - launch_type: FARGATE - network_configuration: - subnets: - - subnet-abcd1234 - security_groups: - - sg-aaaa1111 - - my_security_group - register: task_output - -- name: Stop a task - ecs_task: - operation: stop - cluster: console-sample-app-static-cluster - task_definition: console-sample-app-static-taskdef - task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" -''' -RETURN = ''' -task: - description: details about the task that was started - returned: success - type: complex - contains: - taskArn: - description: The Amazon Resource Name (ARN) that identifies the task. - returned: always - type: str - clusterArn: - description: The Amazon Resource Name (ARN) of the of the cluster that hosts the task. - returned: only when details is true - type: str - taskDefinitionArn: - description: The Amazon Resource Name (ARN) of the task definition. - returned: only when details is true - type: str - containerInstanceArn: - description: The Amazon Resource Name (ARN) of the container running the task. - returned: only when details is true - type: str - overrides: - description: The container overrides set for this task. - returned: only when details is true - type: list - elements: dict - lastStatus: - description: The last recorded status of the task. - returned: only when details is true - type: str - desiredStatus: - description: The desired status of the task. - returned: only when details is true - type: str - containers: - description: The container details. - returned: only when details is true - type: list - elements: dict - startedBy: - description: The used who started the task. - returned: only when details is true - type: str - stoppedReason: - description: The reason why the task was stopped. - returned: only when details is true - type: str - createdAt: - description: The timestamp of when the task was created. - returned: only when details is true - type: str - startedAt: - description: The timestamp of when the task was started. - returned: only when details is true - type: str - stoppedAt: - description: The timestamp of when the task was stopped. - returned: only when details is true - type: str - launchType: - description: The launch type on which to run your task. - returned: always - type: str -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.ec2 import get_ec2_security_group_ids_from_names, ansible_dict_to_boto3_tag_list - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -class EcsExecManager: - """Handles ECS Tasks""" - - def __init__(self, module): - self.module = module - self.ecs = module.client('ecs') - self.ec2 = module.client('ec2') - - def format_network_configuration(self, network_config): - result = dict() - if 'subnets' in network_config: - result['subnets'] = network_config['subnets'] - else: - self.module.fail_json(msg="Network configuration must include subnets") - if 'security_groups' in network_config: - groups = network_config['security_groups'] - if any(not sg.startswith('sg-') for sg in groups): - try: - vpc_id = self.ec2.describe_subnets(SubnetIds=[result['subnets'][0]])['Subnets'][0]['VpcId'] - groups = get_ec2_security_group_ids_from_names(groups, self.ec2, vpc_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't look up security groups") - result['securityGroups'] = groups - return dict(awsvpcConfiguration=result) - - def list_tasks(self, cluster_name, service_name, status): - response = self.ecs.list_tasks( - cluster=cluster_name, - family=service_name, - desiredStatus=status - ) - if len(response['taskArns']) > 0: - for c in response['taskArns']: - if c.endswith(service_name): - return c - return None - - def run_task(self, cluster, task_definition, overrides, count, startedBy, launch_type, tags): - if overrides is None: - overrides = dict() - params = dict(cluster=cluster, taskDefinition=task_definition, - overrides=overrides, count=count, startedBy=startedBy) - if self.module.params['network_configuration']: - params['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) - if launch_type: - params['launchType'] = launch_type - if tags: - params['tags'] = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') - - # TODO: need to check if long arn format enabled. - try: - response = self.ecs.run_task(**params) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't run task") - # include tasks and failures - return response['tasks'] - - def start_task(self, cluster, task_definition, overrides, container_instances, startedBy, tags): - args = dict() - if cluster: - args['cluster'] = cluster - if task_definition: - args['taskDefinition'] = task_definition - if overrides: - args['overrides'] = overrides - if container_instances: - args['containerInstances'] = container_instances - if startedBy: - args['startedBy'] = startedBy - if self.module.params['network_configuration']: - args['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) - if tags: - args['tags'] = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') - try: - response = self.ecs.start_task(**args) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't start task") - # include tasks and failures - return response['tasks'] - - def stop_task(self, cluster, task): - response = self.ecs.stop_task(cluster=cluster, task=task) - return response['task'] - - def ecs_api_handles_launch_type(self): - from distutils.version import LooseVersion - # There doesn't seem to be a nice way to inspect botocore to look - # for attributes (and networkConfiguration is not an explicit argument - # to e.g. ecs.run_task, it's just passed as a keyword argument) - return LooseVersion(botocore.__version__) >= LooseVersion('1.8.4') - - def ecs_task_long_format_enabled(self): - account_support = self.ecs.list_account_settings(name='taskLongArnFormat', effectiveSettings=True) - return account_support['settings'][0]['value'] == 'enabled' - - def ecs_api_handles_tags(self): - from distutils.version import LooseVersion - # There doesn't seem to be a nice way to inspect botocore to look - # for attributes (and networkConfiguration is not an explicit argument - # to e.g. ecs.run_task, it's just passed as a keyword argument) - return LooseVersion(botocore.__version__) >= LooseVersion('1.12.46') - - def ecs_api_handles_network_configuration(self): - from distutils.version import LooseVersion - # There doesn't seem to be a nice way to inspect botocore to look - # for attributes (and networkConfiguration is not an explicit argument - # to e.g. ecs.run_task, it's just passed as a keyword argument) - return LooseVersion(botocore.__version__) >= LooseVersion('1.7.44') - - -def main(): - argument_spec = dict( - operation=dict(required=True, choices=['run', 'start', 'stop']), - cluster=dict(required=False, type='str'), # R S P - task_definition=dict(required=False, type='str'), # R* S* - overrides=dict(required=False, type='dict'), # R S - count=dict(required=False, type='int'), # R - task=dict(required=False, type='str'), # P* - container_instances=dict(required=False, type='list'), # S* - started_by=dict(required=False, type='str'), # R S - network_configuration=dict(required=False, type='dict'), - launch_type=dict(required=False, choices=['EC2', 'FARGATE']), - tags=dict(required=False, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, - required_if=[('launch_type', 'FARGATE', ['network_configuration'])]) - - # Validate Inputs - if module.params['operation'] == 'run': - if 'task_definition' not in module.params and module.params['task_definition'] is None: - module.fail_json(msg="To run a task, a task_definition must be specified") - task_to_list = module.params['task_definition'] - status_type = "RUNNING" - - if module.params['operation'] == 'start': - if 'task_definition' not in module.params and module.params['task_definition'] is None: - module.fail_json(msg="To start a task, a task_definition must be specified") - if 'container_instances' not in module.params and module.params['container_instances'] is None: - module.fail_json(msg="To start a task, container instances must be specified") - task_to_list = module.params['task'] - status_type = "RUNNING" - - if module.params['operation'] == 'stop': - if 'task' not in module.params and module.params['task'] is None: - module.fail_json(msg="To stop a task, a task must be specified") - if 'task_definition' not in module.params and module.params['task_definition'] is None: - module.fail_json(msg="To stop a task, a task definition must be specified") - task_to_list = module.params['task_definition'] - status_type = "STOPPED" - - service_mgr = EcsExecManager(module) - - if module.params['network_configuration'] and not service_mgr.ecs_api_handles_network_configuration(): - module.fail_json(msg='botocore needs to be version 1.7.44 or higher to use network configuration') - - if module.params['launch_type'] and not service_mgr.ecs_api_handles_launch_type(): - module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use launch type') - - if module.params['tags']: - if not service_mgr.ecs_api_handles_tags(): - module.fail_json(msg=missing_required_lib("botocore >= 1.12.46", reason="to use tags")) - if not service_mgr.ecs_task_long_format_enabled(): - module.fail_json(msg="Cannot set task tags: long format task arns are required to set tags") - - existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type) - - results = dict(changed=False) - if module.params['operation'] == 'run': - if existing: - # TBD - validate the rest of the details - results['task'] = existing - else: - if not module.check_mode: - results['task'] = service_mgr.run_task( - module.params['cluster'], - module.params['task_definition'], - module.params['overrides'], - module.params['count'], - module.params['started_by'], - module.params['launch_type'], - module.params['tags'], - ) - results['changed'] = True - - elif module.params['operation'] == 'start': - if existing: - # TBD - validate the rest of the details - results['task'] = existing - else: - if not module.check_mode: - results['task'] = service_mgr.start_task( - module.params['cluster'], - module.params['task_definition'], - module.params['overrides'], - module.params['container_instances'], - module.params['started_by'], - module.params['tags'], - ) - results['changed'] = True - - elif module.params['operation'] == 'stop': - if existing: - results['task'] = existing - else: - if not module.check_mode: - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted - results['task'] = service_mgr.stop_task( - module.params['cluster'], - module.params['task'] - ) - results['changed'] = True - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py b/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py deleted file mode 100644 index 77cdc52884..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_taskdefinition -short_description: register a task definition in ecs -description: - - Registers or deregisters task definitions in the Amazon Web Services (AWS) EC2 Container Service (ECS). -version_added: "2.0" -author: Mark Chance (@Java1Guy) -requirements: [ json, botocore, boto3 ] -options: - state: - description: - - State whether the task definition should exist or be deleted. - required: true - choices: ['present', 'absent'] - type: str - arn: - description: - - The ARN of the task description to delete. - required: false - type: str - family: - description: - - A Name that would be given to the task definition. - required: false - type: str - revision: - description: - - A revision number for the task definition. - required: False - type: int - force_create: - description: - - Always create new task definition. - required: False - version_added: 2.5 - type: bool - containers: - description: - - A list of containers definitions. - required: False - type: list - elements: str - network_mode: - description: - - The Docker networking mode to use for the containers in the task. - - C(awsvpc) mode was added in Ansible 2.5 - - Windows containers must use I(network_mode=default), which will utilize docker NAT networking. - - Setting I(network_mode=default) for a Linux container will use bridge mode. - required: false - default: bridge - choices: [ 'default', 'bridge', 'host', 'none', 'awsvpc' ] - version_added: 2.3 - type: str - task_role_arn: - description: - - The Amazon Resource Name (ARN) of the IAM role that containers in this task can assume. All containers in this task are granted - the permissions that are specified in this role. - required: false - version_added: 2.3 - type: str - execution_role_arn: - description: - - The Amazon Resource Name (ARN) of the task execution role that the Amazon ECS container agent and the Docker daemon can assume. - required: false - version_added: 2.7 - type: str - volumes: - description: - - A list of names of volumes to be attached. - required: False - type: list - elements: dict - suboptions: - name: - type: str - description: The name of the volume. - required: true - launch_type: - description: - - The launch type on which to run your task. - required: false - version_added: 2.7 - type: str - choices: ["EC2", "FARGATE"] - cpu: - description: - - The number of cpu units used by the task. If using the EC2 launch type, this field is optional and any value can be used. - - If using the Fargate launch type, this field is required and you must use one of C(256), C(512), C(1024), C(2048), C(4096). - required: false - version_added: 2.7 - type: str - memory: - description: - - The amount (in MiB) of memory used by the task. If using the EC2 launch type, this field is optional and any value can be used. - - If using the Fargate launch type, this field is required and is limited by the cpu. - required: false - version_added: 2.7 - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Create task definition - ecs_taskdefinition: - containers: - - name: simple-app - cpu: 10 - essential: true - image: "httpd:2.4" - memory: 300 - mountPoints: - - containerPath: /usr/local/apache2/htdocs - sourceVolume: my-vol - portMappings: - - containerPort: 80 - hostPort: 80 - logConfiguration: - logDriver: awslogs - options: - awslogs-group: /ecs/test-cluster-taskdef - awslogs-region: us-west-2 - awslogs-stream-prefix: ecs - - name: busybox - command: - - > - /bin/sh -c "while true; do echo '<html><head><title>Amazon ECS Sample App</title></head><body><div><h1>Amazon ECS Sample App</h1><h2>Congratulations! - </h2><p>Your application is now running on a container in Amazon ECS.</p>' > top; /bin/date > date ; echo '</div></body></html>' > bottom; - cat top date bottom > /usr/local/apache2/htdocs/index.html ; sleep 1; done" - cpu: 10 - entryPoint: - - sh - - "-c" - essential: false - image: busybox - memory: 200 - volumesFrom: - - sourceContainer: simple-app - volumes: - - name: my-vol - family: test-cluster-taskdef - state: present - register: task_output - -- name: Create task definition - ecs_taskdefinition: - family: nginx - containers: - - name: nginx - essential: true - image: "nginx" - portMappings: - - containerPort: 8080 - hostPort: 8080 - cpu: 512 - memory: 1024 - state: present - -- name: Create task definition - ecs_taskdefinition: - family: nginx - containers: - - name: nginx - essential: true - image: "nginx" - portMappings: - - containerPort: 8080 - hostPort: 8080 - launch_type: FARGATE - cpu: 512 - memory: 1024 - state: present - network_mode: awsvpc - -# Create Task Definition with Environment Variables and Secrets -- name: Create task definition - ecs_taskdefinition: - family: nginx - containers: - - name: nginx - essential: true - image: "nginx" - environment: - - name: "PORT" - value: "8080" - secrets: - # For variables stored in Secrets Manager - - name: "NGINX_HOST" - valueFrom: "arn:aws:secretsmanager:us-west-2:123456789012:secret:nginx/NGINX_HOST" - # For variables stored in Parameter Store - - name: "API_KEY" - valueFrom: "arn:aws:ssm:us-west-2:123456789012:parameter/nginx/API_KEY" - launch_type: FARGATE - cpu: 512 - memory: 1GB - state: present - network_mode: awsvpc -''' -RETURN = ''' -taskdefinition: - description: a reflection of the input parameters - type: dict - returned: always -''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils._text import to_text - - -class EcsTaskManager: - """Handles ECS Tasks""" - - def __init__(self, module): - self.module = module - - self.ecs = module.client('ecs') - - def describe_task(self, task_name): - try: - response = self.ecs.describe_task_definition(taskDefinition=task_name) - return response['taskDefinition'] - except botocore.exceptions.ClientError: - return None - - def register_task(self, family, task_role_arn, execution_role_arn, network_mode, container_definitions, volumes, launch_type, cpu, memory): - validated_containers = [] - - # Ensures the number parameters are int as required by boto - for container in container_definitions: - for param in ('memory', 'cpu', 'memoryReservation'): - if param in container: - container[param] = int(container[param]) - - if 'portMappings' in container: - for port_mapping in container['portMappings']: - for port in ('hostPort', 'containerPort'): - if port in port_mapping: - port_mapping[port] = int(port_mapping[port]) - if network_mode == 'awsvpc' and 'hostPort' in port_mapping: - if port_mapping['hostPort'] != port_mapping.get('containerPort'): - self.module.fail_json(msg="In awsvpc network mode, host port must be set to the same as " - "container port or not be set") - - validated_containers.append(container) - - params = dict( - family=family, - taskRoleArn=task_role_arn, - containerDefinitions=container_definitions, - volumes=volumes - ) - if network_mode != 'default': - params['networkMode'] = network_mode - if cpu: - params['cpu'] = cpu - if memory: - params['memory'] = memory - if launch_type: - params['requiresCompatibilities'] = [launch_type] - if execution_role_arn: - params['executionRoleArn'] = execution_role_arn - - try: - response = self.ecs.register_task_definition(**params) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - return response['taskDefinition'] - - def describe_task_definitions(self, family): - data = { - "taskDefinitionArns": [], - "nextToken": None - } - - def fetch(): - # Boto3 is weird about params passed, so only pass nextToken if we have a value - params = { - 'familyPrefix': family - } - - if data['nextToken']: - params['nextToken'] = data['nextToken'] - - result = self.ecs.list_task_definitions(**params) - data['taskDefinitionArns'] += result['taskDefinitionArns'] - data['nextToken'] = result.get('nextToken', None) - return data['nextToken'] is not None - - # Fetch all the arns, possibly across multiple pages - while fetch(): - pass - - # Return the full descriptions of the task definitions, sorted ascending by revision - return list( - sorted( - [self.ecs.describe_task_definition(taskDefinition=arn)['taskDefinition'] for arn in data['taskDefinitionArns']], - key=lambda td: td['revision'] - ) - ) - - def deregister_task(self, taskArn): - response = self.ecs.deregister_task_definition(taskDefinition=taskArn) - return response['taskDefinition'] - - -def main(): - argument_spec = dict( - state=dict(required=True, choices=['present', 'absent']), - arn=dict(required=False, type='str'), - family=dict(required=False, type='str'), - revision=dict(required=False, type='int'), - force_create=dict(required=False, default=False, type='bool'), - containers=dict(required=False, type='list'), - network_mode=dict(required=False, default='bridge', choices=['default', 'bridge', 'host', 'none', 'awsvpc'], type='str'), - task_role_arn=dict(required=False, default='', type='str'), - execution_role_arn=dict(required=False, default='', type='str'), - volumes=dict(required=False, type='list'), - launch_type=dict(required=False, choices=['EC2', 'FARGATE']), - cpu=dict(), - memory=dict(required=False, type='str') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True, - required_if=[('launch_type', 'FARGATE', ['cpu', 'memory'])] - ) - - task_to_describe = None - task_mgr = EcsTaskManager(module) - results = dict(changed=False) - - if module.params['launch_type']: - if not module.botocore_at_least('1.8.4'): - module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use launch_type') - - if module.params['execution_role_arn']: - if not module.botocore_at_least('1.10.44'): - module.fail_json(msg='botocore needs to be version 1.10.44 or higher to use execution_role_arn') - - if module.params['containers']: - for container in module.params['containers']: - for environment in container.get('environment', []): - environment['value'] = to_text(environment['value']) - - if module.params['state'] == 'present': - if 'containers' not in module.params or not module.params['containers']: - module.fail_json(msg="To use task definitions, a list of containers must be specified") - - if 'family' not in module.params or not module.params['family']: - module.fail_json(msg="To use task definitions, a family must be specified") - - network_mode = module.params['network_mode'] - launch_type = module.params['launch_type'] - if launch_type == 'FARGATE' and network_mode != 'awsvpc': - module.fail_json(msg="To use FARGATE launch type, network_mode must be awsvpc") - - family = module.params['family'] - existing_definitions_in_family = task_mgr.describe_task_definitions(module.params['family']) - - if 'revision' in module.params and module.params['revision']: - # The definition specifies revision. We must guarantee that an active revision of that number will result from this. - revision = int(module.params['revision']) - - # A revision has been explicitly specified. Attempt to locate a matching revision - tasks_defs_for_revision = [td for td in existing_definitions_in_family if td['revision'] == revision] - existing = tasks_defs_for_revision[0] if len(tasks_defs_for_revision) > 0 else None - - if existing and existing['status'] != "ACTIVE": - # We cannot reactivate an inactive revision - module.fail_json(msg="A task in family '%s' already exists for revision %d, but it is inactive" % (family, revision)) - elif not existing: - if not existing_definitions_in_family and revision != 1: - module.fail_json(msg="You have specified a revision of %d but a created revision would be 1" % revision) - elif existing_definitions_in_family and existing_definitions_in_family[-1]['revision'] + 1 != revision: - module.fail_json(msg="You have specified a revision of %d but a created revision would be %d" % - (revision, existing_definitions_in_family[-1]['revision'] + 1)) - else: - existing = None - - def _right_has_values_of_left(left, right): - # Make sure the values are equivalent for everything left has - for k, v in left.items(): - if not ((not v and (k not in right or not right[k])) or (k in right and v == right[k])): - # We don't care about list ordering because ECS can change things - if isinstance(v, list) and k in right: - left_list = v - right_list = right[k] or [] - - if len(left_list) != len(right_list): - return False - - for list_val in left_list: - if list_val not in right_list: - return False - else: - return False - - # Make sure right doesn't have anything that left doesn't - for k, v in right.items(): - if v and k not in left: - return False - - return True - - def _task_definition_matches(requested_volumes, requested_containers, requested_task_role_arn, existing_task_definition): - if td['status'] != "ACTIVE": - return None - - if requested_task_role_arn != td.get('taskRoleArn', ""): - return None - - existing_volumes = td.get('volumes', []) or [] - - if len(requested_volumes) != len(existing_volumes): - # Nope. - return None - - if len(requested_volumes) > 0: - for requested_vol in requested_volumes: - found = False - - for actual_vol in existing_volumes: - if _right_has_values_of_left(requested_vol, actual_vol): - found = True - break - - if not found: - return None - - existing_containers = td.get('containerDefinitions', []) or [] - - if len(requested_containers) != len(existing_containers): - # Nope. - return None - - for requested_container in requested_containers: - found = False - - for actual_container in existing_containers: - if _right_has_values_of_left(requested_container, actual_container): - found = True - break - - if not found: - return None - - return existing_task_definition - - # No revision explicitly specified. Attempt to find an active, matching revision that has all the properties requested - for td in existing_definitions_in_family: - requested_volumes = module.params['volumes'] or [] - requested_containers = module.params['containers'] or [] - requested_task_role_arn = module.params['task_role_arn'] - existing = _task_definition_matches(requested_volumes, requested_containers, requested_task_role_arn, td) - - if existing: - break - - if existing and not module.params.get('force_create'): - # Awesome. Have an existing one. Nothing to do. - results['taskdefinition'] = existing - else: - if not module.check_mode: - # Doesn't exist. create it. - volumes = module.params.get('volumes', []) or [] - results['taskdefinition'] = task_mgr.register_task(module.params['family'], - module.params['task_role_arn'], - module.params['execution_role_arn'], - module.params['network_mode'], - module.params['containers'], - volumes, - module.params['launch_type'], - module.params['cpu'], - module.params['memory']) - results['changed'] = True - - elif module.params['state'] == 'absent': - # When de-registering a task definition, we can specify the ARN OR the family and revision. - if module.params['state'] == 'absent': - if 'arn' in module.params and module.params['arn'] is not None: - task_to_describe = module.params['arn'] - elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and \ - module.params['revision'] is not None: - task_to_describe = module.params['family'] + ":" + str(module.params['revision']) - else: - module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") - - existing = task_mgr.describe_task(task_to_describe) - - if not existing: - pass - else: - # It exists, so we should delete it and mark changed. Return info about the task definition deleted - results['taskdefinition'] = existing - if 'status' in existing and existing['status'] == "INACTIVE": - results['changed'] = False - else: - if not module.check_mode: - task_mgr.deregister_task(task_to_describe) - results['changed'] = True - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition_info.py b/lib/ansible/modules/cloud/amazon/ecs_taskdefinition_info.py deleted file mode 100644 index 815339fa07..0000000000 --- a/lib/ansible/modules/cloud/amazon/ecs_taskdefinition_info.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_taskdefinition_info -short_description: Describe a task definition in ECS -notes: - - For details of the parameters and returns see - U(http://boto3.readthedocs.io/en/latest/reference/services/ecs.html#ECS.Client.describe_task_definition) - - This module was called C(ecs_taskdefinition_facts) before Ansible 2.9. The usage did not change. -description: - - Describes a task definition in ECS. -version_added: "2.5" -author: - - Gustavo Maia (@gurumaia) - - Mark Chance (@Java1Guy) - - Darek Kaczynski (@kaczynskid) -requirements: [ json, botocore, boto3 ] -options: - task_definition: - description: - - The name of the task definition to get details for - required: true - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- ecs_taskdefinition_info: - task_definition: test-td -''' - -RETURN = ''' -container_definitions: - description: Returns a list of complex objects representing the containers - returned: success - type: complex - contains: - name: - description: The name of a container. - returned: always - type: str - image: - description: The image used to start a container. - returned: always - type: str - cpu: - description: The number of cpu units reserved for the container. - returned: always - type: int - memoryReservation: - description: The soft limit (in MiB) of memory to reserve for the container. - returned: when present - type: int - links: - description: Links to other containers. - returned: when present - type: str - portMappings: - description: The list of port mappings for the container. - returned: always - type: complex - contains: - containerPort: - description: The port number on the container. - returned: when present - type: int - hostPort: - description: The port number on the container instance to reserve for your container. - returned: when present - type: int - protocol: - description: The protocol used for the port mapping. - returned: when present - type: str - essential: - description: Whether this is an essential container or not. - returned: always - type: bool - entryPoint: - description: The entry point that is passed to the container. - returned: when present - type: str - command: - description: The command that is passed to the container. - returned: when present - type: str - environment: - description: The environment variables to pass to a container. - returned: always - type: complex - contains: - name: - description: The name of the environment variable. - returned: when present - type: str - value: - description: The value of the environment variable. - returned: when present - type: str - mountPoints: - description: The mount points for data volumes in your container. - returned: always - type: complex - contains: - sourceVolume: - description: The name of the volume to mount. - returned: when present - type: str - containerPath: - description: The path on the container to mount the host volume at. - returned: when present - type: str - readOnly: - description: If this value is true , the container has read-only access to the volume. - If this value is false , then the container can write to the volume. - returned: when present - type: bool - volumesFrom: - description: Data volumes to mount from another container. - returned: always - type: complex - contains: - sourceContainer: - description: The name of another container within the same task definition to mount volumes from. - returned: when present - type: str - readOnly: - description: If this value is true , the container has read-only access to the volume. - If this value is false , then the container can write to the volume. - returned: when present - type: bool - hostname: - description: The hostname to use for your container. - returned: when present - type: str - user: - description: The user name to use inside the container. - returned: when present - type: str - workingDirectory: - description: The working directory in which to run commands inside the container. - returned: when present - type: str - disableNetworking: - description: When this parameter is true, networking is disabled within the container. - returned: when present - type: bool - privileged: - description: When this parameter is true, the container is given elevated - privileges on the host container instance (similar to the root user). - returned: when present - type: bool - readonlyRootFilesystem: - description: When this parameter is true, the container is given read-only access to its root file system. - returned: when present - type: bool - dnsServers: - description: A list of DNS servers that are presented to the container. - returned: when present - type: str - dnsSearchDomains: - description: A list of DNS search domains that are presented to the container. - returned: when present - type: str - extraHosts: - description: A list of hostnames and IP address mappings to append to the /etc/hosts file on the container. - returned: when present - type: complex - contains: - hostname: - description: The hostname to use in the /etc/hosts entry. - returned: when present - type: str - ipAddress: - description: The IP address to use in the /etc/hosts entry. - returned: when present - type: str - dockerSecurityOptions: - description: A list of strings to provide custom labels for SELinux and AppArmor multi-level security systems. - returned: when present - type: str - dockerLabels: - description: A key/value map of labels to add to the container. - returned: when present - type: str - ulimits: - description: A list of ulimits to set in the container. - returned: when present - type: complex - contains: - name: - description: The type of the ulimit . - returned: when present - type: str - softLimit: - description: The soft limit for the ulimit type. - returned: when present - type: int - hardLimit: - description: The hard limit for the ulimit type. - returned: when present - type: int - logConfiguration: - description: The log configuration specification for the container. - returned: when present - type: str - options: - description: The configuration options to send to the log driver. - returned: when present - type: str - -family: - description: The family of your task definition, used as the definition name - returned: always - type: str -task_definition_arn: - description: ARN of the task definition - returned: always - type: str -task_role_arn: - description: The ARN of the IAM role that containers in this task can assume - returned: when role is set - type: str -network_mode: - description: Network mode for the containers - returned: always - type: str -revision: - description: Revision number that was queried - returned: always - type: int -volumes: - description: The list of volumes in a task - returned: always - type: complex - contains: - name: - description: The name of the volume. - returned: when present - type: str - host: - description: The contents of the host parameter determine whether your data volume - persists on the host container instance and where it is stored. - returned: when present - type: bool - source_path: - description: The path on the host container instance that is presented to the container. - returned: when present - type: str -status: - description: The status of the task definition - returned: always - type: str -requires_attributes: - description: The container instance attributes required by your task - returned: when present - type: complex - contains: - name: - description: The name of the attribute. - returned: when present - type: str - value: - description: The value of the attribute. - returned: when present - type: str - targetType: - description: The type of the target with which to attach the attribute. - returned: when present - type: str - targetId: - description: The ID of the target. - returned: when present - type: str -placement_constraints: - description: A list of placement constraint objects to use for tasks - returned: always - type: complex - contains: - type: - description: The type of constraint. - returned: when present - type: str - expression: - description: A cluster query language expression to apply to the constraint. - returned: when present - type: str -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -def main(): - argument_spec = dict( - task_definition=dict(required=True, type='str') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ecs_taskdefinition_facts': - module.deprecate("The 'ecs_taskdefinition_facts' module has been renamed to 'ecs_taskdefinition_info'", version='2.13') - - ecs = module.client('ecs') - - try: - ecs_td = ecs.describe_task_definition(taskDefinition=module.params['task_definition'])['taskDefinition'] - except botocore.exceptions.ClientError: - ecs_td = {} - - module.exit_json(changed=False, **camel_dict_to_snake_dict(ecs_td)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/efs.py b/lib/ansible/modules/cloud/amazon/efs.py deleted file mode 100644 index f93527268a..0000000000 --- a/lib/ansible/modules/cloud/amazon/efs.py +++ /dev/null @@ -1,758 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: efs -short_description: create and maintain EFS file systems -description: - - Module allows create, search and destroy Amazon EFS file systems. -version_added: "2.2" -requirements: [ boto3 ] -author: - - "Ryan Sydnor (@ryansydnor)" - - "Artem Kazakov (@akazakov)" -options: - encrypt: - description: - - If I(encrypt=true) creates an encrypted file system. This can not be modified after the file system is created. - type: bool - default: false - version_added: 2.5 - kms_key_id: - description: - - The id of the AWS KMS CMK that will be used to protect the encrypted file system. This parameter is only - required if you want to use a non-default CMK. If this parameter is not specified, the default CMK for - Amazon EFS is used. The key id can be Key ID, Key ID ARN, Key Alias or Key Alias ARN. - version_added: 2.5 - type: str - purge_tags: - description: - - If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. If the I(tags) parameter - is not set then tags will not be modified. - type: bool - default: true - version_added: 2.5 - state: - description: - - Allows to create, search and destroy Amazon EFS file system. - default: 'present' - choices: ['present', 'absent'] - type: str - name: - description: - - Creation Token of Amazon EFS file system. Required for create and update. Either name or ID required for delete. - type: str - id: - description: - - ID of Amazon EFS. Either name or ID required for delete. - type: str - performance_mode: - description: - - File system's performance mode to use. Only takes effect during creation. - default: 'general_purpose' - choices: ['general_purpose', 'max_io'] - type: str - tags: - description: - - "List of tags of Amazon EFS. Should be defined as dictionary - In case of 'present' state with list of tags and existing EFS (matched by 'name'), tags of EFS will be replaced with provided data." - type: dict - targets: - description: - - "List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: - This data may be modified for existing EFS using state 'present' and new list of mount targets." - type: list - elements: dict - suboptions: - subnet_id: - required: true - description: The ID of the subnet to add the mount target in. - ip_address: - type: str - description: A valid IPv4 address within the address range of the specified subnet. - security_groups: - type: list - elements: str - description: List of security group IDs, of the form 'sg-xxxxxxxx'. These must be for the same VPC as subnet specified - throughput_mode: - description: - - The throughput_mode for the file system to be created. - - Requires botocore >= 1.10.57 - choices: ['bursting', 'provisioned'] - version_added: 2.8 - type: str - provisioned_throughput_in_mibps: - description: - - If the throughput_mode is provisioned, select the amount of throughput to provisioned in Mibps. - - Requires botocore >= 1.10.57 - type: float - version_added: 2.8 - wait: - description: - - "In case of 'present' state should wait for EFS 'available' life cycle state (of course, if current state not 'deleting' or 'deleted') - In case of 'absent' state should wait for EFS 'deleted' life cycle state" - type: bool - default: false - wait_timeout: - description: - - How long the module should wait (in seconds) for desired state before returning. Zero means wait as long as necessary. - default: 0 - type: int - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# EFS provisioning -- efs: - state: present - name: myTestEFS - tags: - Name: myTestNameTag - purpose: file-storage - targets: - - subnet_id: subnet-748c5d03 - security_groups: [ "sg-1a2b3c4d" ] - -# Modifying EFS data -- efs: - state: present - name: myTestEFS - tags: - name: myAnotherTestTag - targets: - - subnet_id: subnet-7654fdca - security_groups: [ "sg-4c5d6f7a" ] - -# Deleting EFS -- efs: - state: absent - name: myTestEFS -''' - -RETURN = ''' -creation_time: - description: timestamp of creation date - returned: always - type: str - sample: "2015-11-16 07:30:57-05:00" -creation_token: - description: EFS creation token - returned: always - type: str - sample: "console-88609e04-9a0e-4a2e-912c-feaa99509961" -file_system_id: - description: ID of the file system - returned: always - type: str - sample: "fs-xxxxxxxx" -life_cycle_state: - description: state of the EFS file system - returned: always - type: str - sample: "creating, available, deleting, deleted" -mount_point: - description: url of file system with leading dot from the time when AWS EFS required to add a region suffix to the address - returned: always - type: str - sample: ".fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/" -filesystem_address: - description: url of file system valid for use with mount - returned: always - type: str - sample: "fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/" -mount_targets: - description: list of mount targets - returned: always - type: list - sample: - [ - { - "file_system_id": "fs-a7ad440e", - "ip_address": "172.31.17.173", - "life_cycle_state": "available", - "mount_target_id": "fsmt-d8907871", - "network_interface_id": "eni-6e387e26", - "owner_id": "740748460359", - "security_groups": [ - "sg-a30b22c6" - ], - "subnet_id": "subnet-e265c895" - }, - ... - ] -name: - description: name of the file system - returned: always - type: str - sample: "my-efs" -number_of_mount_targets: - description: the number of targets mounted - returned: always - type: int - sample: 3 -owner_id: - description: AWS account ID of EFS owner - returned: always - type: str - sample: "XXXXXXXXXXXX" -size_in_bytes: - description: size of the file system in bytes as of a timestamp - returned: always - type: dict - sample: - { - "timestamp": "2015-12-21 13:59:59-05:00", - "value": 12288 - } -performance_mode: - description: performance mode of the file system - returned: always - type: str - sample: "generalPurpose" -tags: - description: tags on the efs instance - returned: always - type: dict - sample: - { - "name": "my-efs", - "key": "Value" - } - -''' - -from time import sleep -from time import time as timestamp - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError as e: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (compare_aws_tags, camel_dict_to_snake_dict, - ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict) - - -def _index_by_key(key, items): - return dict((item[key], item) for item in items) - - -class EFSConnection(object): - - DEFAULT_WAIT_TIMEOUT_SECONDS = 0 - - STATE_CREATING = 'creating' - STATE_AVAILABLE = 'available' - STATE_DELETING = 'deleting' - STATE_DELETED = 'deleted' - - def __init__(self, module): - self.connection = module.client('efs') - region = module.region - - self.module = module - self.region = region - self.wait = module.params.get('wait') - self.wait_timeout = module.params.get('wait_timeout') - - def get_file_systems(self, **kwargs): - """ - Returns generator of file systems including all attributes of FS - """ - items = iterate_all( - 'FileSystems', - self.connection.describe_file_systems, - **kwargs - ) - for item in items: - item['Name'] = item['CreationToken'] - item['CreationTime'] = str(item['CreationTime']) - """ - In the time when MountPoint was introduced there was a need to add a suffix of network path before one could use it - AWS updated it and now there is no need to add a suffix. MountPoint is left for back-compatibility purpose - And new FilesystemAddress variable is introduced for direct use with other modules (e.g. mount) - AWS documentation is available here: - https://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html - """ - item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) - item['FilesystemAddress'] = '%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) - if 'Timestamp' in item['SizeInBytes']: - item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp']) - if item['LifeCycleState'] == self.STATE_AVAILABLE: - item['Tags'] = self.get_tags(FileSystemId=item['FileSystemId']) - item['MountTargets'] = list(self.get_mount_targets(FileSystemId=item['FileSystemId'])) - else: - item['Tags'] = {} - item['MountTargets'] = [] - yield item - - def get_tags(self, **kwargs): - """ - Returns tag list for selected instance of EFS - """ - tags = self.connection.describe_tags(**kwargs)['Tags'] - return tags - - def get_mount_targets(self, **kwargs): - """ - Returns mount targets for selected instance of EFS - """ - targets = iterate_all( - 'MountTargets', - self.connection.describe_mount_targets, - **kwargs - ) - for target in targets: - if target['LifeCycleState'] == self.STATE_AVAILABLE: - target['SecurityGroups'] = list(self.get_security_groups( - MountTargetId=target['MountTargetId'] - )) - else: - target['SecurityGroups'] = [] - yield target - - def get_security_groups(self, **kwargs): - """ - Returns security groups for selected instance of EFS - """ - return iterate_all( - 'SecurityGroups', - self.connection.describe_mount_target_security_groups, - **kwargs - ) - - def get_file_system_id(self, name): - """ - Returns ID of instance by instance name - """ - info = first_or_default(iterate_all( - 'FileSystems', - self.connection.describe_file_systems, - CreationToken=name - )) - return info and info['FileSystemId'] or None - - def get_file_system_state(self, name, file_system_id=None): - """ - Returns state of filesystem by EFS id/name - """ - info = first_or_default(iterate_all( - 'FileSystems', - self.connection.describe_file_systems, - CreationToken=name, - FileSystemId=file_system_id - )) - return info and info['LifeCycleState'] or self.STATE_DELETED - - def get_mount_targets_in_state(self, file_system_id, states=None): - """ - Returns states of mount targets of selected EFS with selected state(s) (optional) - """ - targets = iterate_all( - 'MountTargets', - self.connection.describe_mount_targets, - FileSystemId=file_system_id - ) - - if states: - if not isinstance(states, list): - states = [states] - targets = filter(lambda target: target['LifeCycleState'] in states, targets) - - return list(targets) - - def supports_provisioned_mode(self): - """ - Ensure boto3 includes provisioned throughput mode feature - """ - return hasattr(self.connection, 'update_file_system') - - def get_throughput_mode(self, **kwargs): - """ - Returns throughput mode for selected EFS instance - """ - info = first_or_default(iterate_all( - 'FileSystems', - self.connection.describe_file_systems, - **kwargs - )) - - return info and info['ThroughputMode'] or None - - def get_provisioned_throughput_in_mibps(self, **kwargs): - """ - Returns throughput mode for selected EFS instance - """ - info = first_or_default(iterate_all( - 'FileSystems', - self.connection.describe_file_systems, - **kwargs - )) - return info.get('ProvisionedThroughputInMibps', None) - - def create_file_system(self, name, performance_mode, encrypt, kms_key_id, throughput_mode, provisioned_throughput_in_mibps): - """ - Creates new filesystem with selected name - """ - changed = False - state = self.get_file_system_state(name) - params = {} - params['CreationToken'] = name - params['PerformanceMode'] = performance_mode - if encrypt: - params['Encrypted'] = encrypt - if kms_key_id is not None: - params['KmsKeyId'] = kms_key_id - if throughput_mode: - if self.supports_provisioned_mode(): - params['ThroughputMode'] = throughput_mode - else: - self.module.fail_json(msg="throughput_mode parameter requires botocore >= 1.10.57") - if provisioned_throughput_in_mibps: - if self.supports_provisioned_mode(): - params['ProvisionedThroughputInMibps'] = provisioned_throughput_in_mibps - else: - self.module.fail_json(msg="provisioned_throughput_in_mibps parameter requires botocore >= 1.10.57") - - if state in [self.STATE_DELETING, self.STATE_DELETED]: - wait_for( - lambda: self.get_file_system_state(name), - self.STATE_DELETED - ) - try: - self.connection.create_file_system(**params) - changed = True - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Unable to create file system.") - - # we always wait for the state to be available when creating. - # if we try to take any actions on the file system before it's available - # we'll throw errors - wait_for( - lambda: self.get_file_system_state(name), - self.STATE_AVAILABLE, - self.wait_timeout - ) - - return changed - - def update_file_system(self, name, throughput_mode, provisioned_throughput_in_mibps): - """ - Update filesystem with new throughput settings - """ - changed = False - state = self.get_file_system_state(name) - if state in [self.STATE_AVAILABLE, self.STATE_CREATING]: - fs_id = self.get_file_system_id(name) - current_mode = self.get_throughput_mode(FileSystemId=fs_id) - current_throughput = self.get_provisioned_throughput_in_mibps(FileSystemId=fs_id) - params = dict() - if throughput_mode and throughput_mode != current_mode: - params['ThroughputMode'] = throughput_mode - if provisioned_throughput_in_mibps and provisioned_throughput_in_mibps != current_throughput: - params['ProvisionedThroughputInMibps'] = provisioned_throughput_in_mibps - if len(params) > 0: - wait_for( - lambda: self.get_file_system_state(name), - self.STATE_AVAILABLE, - self.wait_timeout - ) - try: - self.connection.update_file_system(FileSystemId=fs_id, **params) - changed = True - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Unable to update file system.") - return changed - - def converge_file_system(self, name, tags, purge_tags, targets, throughput_mode, provisioned_throughput_in_mibps): - """ - Change attributes (mount targets and tags) of filesystem by name - """ - result = False - fs_id = self.get_file_system_id(name) - - if tags is not None: - tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(self.get_tags(FileSystemId=fs_id)), tags, purge_tags) - - if tags_to_delete: - try: - self.connection.delete_tags( - FileSystemId=fs_id, - TagKeys=tags_to_delete - ) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Unable to delete tags.") - - result = True - - if tags_need_modify: - try: - self.connection.create_tags( - FileSystemId=fs_id, - Tags=ansible_dict_to_boto3_tag_list(tags_need_modify) - ) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Unable to create tags.") - - result = True - - if targets is not None: - incomplete_states = [self.STATE_CREATING, self.STATE_DELETING] - wait_for( - lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), - 0 - ) - current_targets = _index_by_key('SubnetId', self.get_mount_targets(FileSystemId=fs_id)) - targets = _index_by_key('SubnetId', targets) - - targets_to_create, intersection, targets_to_delete = dict_diff(current_targets, - targets, True) - - # To modify mount target it should be deleted and created again - changed = [sid for sid in intersection if not targets_equal(['SubnetId', 'IpAddress', 'NetworkInterfaceId'], - current_targets[sid], targets[sid])] - targets_to_delete = list(targets_to_delete) + changed - targets_to_create = list(targets_to_create) + changed - - if targets_to_delete: - for sid in targets_to_delete: - self.connection.delete_mount_target( - MountTargetId=current_targets[sid]['MountTargetId'] - ) - wait_for( - lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), - 0 - ) - result = True - - if targets_to_create: - for sid in targets_to_create: - self.connection.create_mount_target( - FileSystemId=fs_id, - **targets[sid] - ) - wait_for( - lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), - 0, - self.wait_timeout - ) - result = True - - # If no security groups were passed into the module, then do not change it. - security_groups_to_update = [sid for sid in intersection if - 'SecurityGroups' in targets[sid] and - current_targets[sid]['SecurityGroups'] != targets[sid]['SecurityGroups']] - - if security_groups_to_update: - for sid in security_groups_to_update: - self.connection.modify_mount_target_security_groups( - MountTargetId=current_targets[sid]['MountTargetId'], - SecurityGroups=targets[sid].get('SecurityGroups', None) - ) - result = True - - return result - - def delete_file_system(self, name, file_system_id=None): - """ - Removes EFS instance by id/name - """ - result = False - state = self.get_file_system_state(name, file_system_id) - if state in [self.STATE_CREATING, self.STATE_AVAILABLE]: - wait_for( - lambda: self.get_file_system_state(name), - self.STATE_AVAILABLE - ) - if not file_system_id: - file_system_id = self.get_file_system_id(name) - self.delete_mount_targets(file_system_id) - self.connection.delete_file_system(FileSystemId=file_system_id) - result = True - - if self.wait: - wait_for( - lambda: self.get_file_system_state(name), - self.STATE_DELETED, - self.wait_timeout - ) - - return result - - def delete_mount_targets(self, file_system_id): - """ - Removes mount targets by EFS id - """ - wait_for( - lambda: len(self.get_mount_targets_in_state(file_system_id, self.STATE_CREATING)), - 0 - ) - - targets = self.get_mount_targets_in_state(file_system_id, self.STATE_AVAILABLE) - for target in targets: - self.connection.delete_mount_target(MountTargetId=target['MountTargetId']) - - wait_for( - lambda: len(self.get_mount_targets_in_state(file_system_id, self.STATE_DELETING)), - 0 - ) - - return len(targets) > 0 - - -def iterate_all(attr, map_method, **kwargs): - """ - Method creates iterator from result set - """ - args = dict((key, value) for (key, value) in kwargs.items() if value is not None) - wait = 1 - while True: - try: - data = map_method(**args) - for elm in data[attr]: - yield elm - if 'NextMarker' in data: - args['Marker'] = data['Nextmarker'] - continue - break - except ClientError as e: - if e.response['Error']['Code'] == "ThrottlingException" and wait < 600: - sleep(wait) - wait = wait * 2 - continue - else: - raise - - -def targets_equal(keys, a, b): - """ - Method compare two mount targets by specified attributes - """ - for key in keys: - if key in b and a[key] != b[key]: - return False - - return True - - -def dict_diff(dict1, dict2, by_key=False): - """ - Helper method to calculate difference of two dictionaries - """ - keys1 = set(dict1.keys() if by_key else dict1.items()) - keys2 = set(dict2.keys() if by_key else dict2.items()) - - intersection = keys1 & keys2 - - return keys2 ^ intersection, intersection, keys1 ^ intersection - - -def first_or_default(items, default=None): - """ - Helper method to fetch first element of list (if exists) - """ - for item in items: - return item - return default - - -def wait_for(callback, value, timeout=EFSConnection.DEFAULT_WAIT_TIMEOUT_SECONDS): - """ - Helper method to wait for desired value returned by callback method - """ - wait_start = timestamp() - while True: - if callback() != value: - if timeout != 0 and (timestamp() - wait_start > timeout): - raise RuntimeError('Wait timeout exceeded (' + str(timeout) + ' sec)') - else: - sleep(5) - continue - break - - -def main(): - """ - Module action handler - """ - argument_spec = dict( - encrypt=dict(required=False, type="bool", default=False), - state=dict(required=False, type='str', choices=["present", "absent"], default="present"), - kms_key_id=dict(required=False, type='str', default=None), - purge_tags=dict(default=True, type='bool'), - id=dict(required=False, type='str', default=None), - name=dict(required=False, type='str', default=None), - tags=dict(required=False, type="dict", default={}), - targets=dict(required=False, type="list", default=[]), - performance_mode=dict(required=False, type='str', choices=["general_purpose", "max_io"], default="general_purpose"), - throughput_mode=dict(required=False, type='str', choices=["bursting", "provisioned"], default=None), - provisioned_throughput_in_mibps=dict(required=False, type='float'), - wait=dict(required=False, type="bool", default=False), - wait_timeout=dict(required=False, type="int", default=0) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - - connection = EFSConnection(module) - - name = module.params.get('name') - fs_id = module.params.get('id') - tags = module.params.get('tags') - target_translations = { - 'ip_address': 'IpAddress', - 'security_groups': 'SecurityGroups', - 'subnet_id': 'SubnetId' - } - targets = [dict((target_translations[key], value) for (key, value) in x.items()) for x in module.params.get('targets')] - performance_mode_translations = { - 'general_purpose': 'generalPurpose', - 'max_io': 'maxIO' - } - encrypt = module.params.get('encrypt') - kms_key_id = module.params.get('kms_key_id') - performance_mode = performance_mode_translations[module.params.get('performance_mode')] - purge_tags = module.params.get('purge_tags') - throughput_mode = module.params.get('throughput_mode') - provisioned_throughput_in_mibps = module.params.get('provisioned_throughput_in_mibps') - state = str(module.params.get('state')).lower() - changed = False - - if state == 'present': - if not name: - module.fail_json(msg='Name parameter is required for create') - - changed = connection.create_file_system(name, performance_mode, encrypt, kms_key_id, throughput_mode, provisioned_throughput_in_mibps) - if connection.supports_provisioned_mode(): - changed = connection.update_file_system(name, throughput_mode, provisioned_throughput_in_mibps) or changed - changed = connection.converge_file_system(name=name, tags=tags, purge_tags=purge_tags, targets=targets, - throughput_mode=throughput_mode, provisioned_throughput_in_mibps=provisioned_throughput_in_mibps) or changed - result = first_or_default(connection.get_file_systems(CreationToken=name)) - - elif state == 'absent': - if not name and not fs_id: - module.fail_json(msg='Either name or id parameter is required for delete') - - changed = connection.delete_file_system(name, fs_id) - result = None - if result: - result = camel_dict_to_snake_dict(result) - module.exit_json(changed=changed, efs=result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/efs_info.py b/lib/ansible/modules/cloud/amazon/efs_info.py deleted file mode 100644 index 5238f041e0..0000000000 --- a/lib/ansible/modules/cloud/amazon/efs_info.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: efs_info -short_description: Get information about Amazon EFS file systems -description: - - This module can be used to search Amazon EFS file systems. - - This module was called C(efs_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(efs_info) module no longer returns C(ansible_facts)! -version_added: "2.2" -requirements: [ boto3 ] -author: - - "Ryan Sydnor (@ryansydnor)" -options: - name: - description: - - Creation Token of Amazon EFS file system. - aliases: [ creation_token ] - type: str - id: - description: - - ID of Amazon EFS. - type: str - tags: - description: - - List of tags of Amazon EFS. Should be defined as dictionary. - type: dict - targets: - description: - - List of targets on which to filter the returned results. - - Result must match all of the specified targets, each of which can be a security group ID, a subnet ID or an IP address. - type: list - elements: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Find all existing efs - efs_info: - register: result - -- name: Find efs using id - efs_info: - id: fs-1234abcd - register: result - -- name: Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a' - efs_info: - tags: - Name: myTestNameTag - targets: - - subnet-1a2b3c4d - - sg-4d3c2b1a - register: result - -- debug: - msg: "{{ result['efs'] }}" -''' - -RETURN = ''' -creation_time: - description: timestamp of creation date - returned: always - type: str - sample: "2015-11-16 07:30:57-05:00" -creation_token: - description: EFS creation token - returned: always - type: str - sample: console-88609e04-9a0e-4a2e-912c-feaa99509961 -file_system_id: - description: ID of the file system - returned: always - type: str - sample: fs-xxxxxxxx -life_cycle_state: - description: state of the EFS file system - returned: always - type: str - sample: creating, available, deleting, deleted -mount_point: - description: url of file system with leading dot from the time AWS EFS required to add network suffix to EFS address - returned: always - type: str - sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/ -filesystem_address: - description: url of file system - returned: always - type: str - sample: fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/ -mount_targets: - description: list of mount targets - returned: always - type: list - sample: - [ - { - "file_system_id": "fs-a7ad440e", - "ip_address": "172.31.17.173", - "life_cycle_state": "available", - "mount_target_id": "fsmt-d8907871", - "network_interface_id": "eni-6e387e26", - "owner_id": "740748460359", - "security_groups": [ - "sg-a30b22c6" - ], - "subnet_id": "subnet-e265c895" - }, - ... - ] -name: - description: name of the file system - returned: always - type: str - sample: my-efs -number_of_mount_targets: - description: the number of targets mounted - returned: always - type: int - sample: 3 -owner_id: - description: AWS account ID of EFS owner - returned: always - type: str - sample: XXXXXXXXXXXX -size_in_bytes: - description: size of the file system in bytes as of a timestamp - returned: always - type: dict - sample: - { - "timestamp": "2015-12-21 13:59:59-05:00", - "value": 12288 - } -performance_mode: - description: performance mode of the file system - returned: always - type: str - sample: "generalPurpose" -throughput_mode: - description: mode of throughput for the file system - returned: when botocore >= 1.10.57 - type: str - sample: "bursting" -provisioned_throughput_in_mibps: - description: throughput provisioned in Mibps - returned: when botocore >= 1.10.57 and throughput_mode is set to "provisioned" - type: float - sample: 15.0 -tags: - description: tags on the efs instance - returned: always - type: dict - sample: - { - "name": "my-efs", - "key": "Value" - } - -''' - - -from collections import defaultdict - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import get_aws_connection_info, AWSRetry -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict -from ansible.module_utils._text import to_native - - -class EFSConnection(object): - STATE_CREATING = 'creating' - STATE_AVAILABLE = 'available' - STATE_DELETING = 'deleting' - STATE_DELETED = 'deleted' - - def __init__(self, module): - try: - self.connection = module.client('efs') - self.module = module - except Exception as e: - module.fail_json(msg="Failed to connect to AWS: %s" % to_native(e)) - - self.region = get_aws_connection_info(module, boto3=True)[0] - - @AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException']) - def list_file_systems(self, **kwargs): - """ - Returns generator of file systems including all attributes of FS - """ - paginator = self.connection.get_paginator('describe_file_systems') - return paginator.paginate(**kwargs).build_full_result()['FileSystems'] - - @AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException']) - def get_tags(self, file_system_id): - """ - Returns tag list for selected instance of EFS - """ - paginator = self.connection.get_paginator('describe_tags') - return boto3_tag_list_to_ansible_dict(paginator.paginate(FileSystemId=file_system_id).build_full_result()['Tags']) - - @AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException']) - def get_mount_targets(self, file_system_id): - """ - Returns mount targets for selected instance of EFS - """ - paginator = self.connection.get_paginator('describe_mount_targets') - return paginator.paginate(FileSystemId=file_system_id).build_full_result()['MountTargets'] - - @AWSRetry.jittered_backoff(catch_extra_error_codes=['ThrottlingException']) - def get_security_groups(self, mount_target_id): - """ - Returns security groups for selected instance of EFS - """ - return self.connection.describe_mount_target_security_groups(MountTargetId=mount_target_id)['SecurityGroups'] - - def get_mount_targets_data(self, file_systems): - for item in file_systems: - if item['life_cycle_state'] == self.STATE_AVAILABLE: - try: - mount_targets = self.get_mount_targets(item['file_system_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get EFS targets") - for mt in mount_targets: - item['mount_targets'].append(camel_dict_to_snake_dict(mt)) - return file_systems - - def get_security_groups_data(self, file_systems): - for item in file_systems: - if item['life_cycle_state'] == self.STATE_AVAILABLE: - for target in item['mount_targets']: - if target['life_cycle_state'] == self.STATE_AVAILABLE: - try: - target['security_groups'] = self.get_security_groups(target['mount_target_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get EFS security groups") - else: - target['security_groups'] = [] - else: - item['tags'] = {} - item['mount_targets'] = [] - return file_systems - - def get_file_systems(self, file_system_id=None, creation_token=None): - kwargs = dict() - if file_system_id: - kwargs['FileSystemId'] = file_system_id - if creation_token: - kwargs['CreationToken'] = creation_token - try: - file_systems = self.list_file_systems(**kwargs) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get EFS file systems") - - results = list() - for item in file_systems: - item['CreationTime'] = str(item['CreationTime']) - """ - In the time when MountPoint was introduced there was a need to add a suffix of network path before one could use it - AWS updated it and now there is no need to add a suffix. MountPoint is left for back-compatibility purpose - And new FilesystemAddress variable is introduced for direct use with other modules (e.g. mount) - AWS documentation is available here: - U(https://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html) - """ - item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) - item['FilesystemAddress'] = '%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) - - if 'Timestamp' in item['SizeInBytes']: - item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp']) - result = camel_dict_to_snake_dict(item) - result['tags'] = {} - result['mount_targets'] = [] - # Set tags *after* doing camel to snake - if result['life_cycle_state'] == self.STATE_AVAILABLE: - try: - result['tags'] = self.get_tags(result['file_system_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get EFS tags") - results.append(result) - return results - - -def prefix_to_attr(attr_id): - """ - Helper method to convert ID prefix to mount target attribute - """ - attr_by_prefix = { - 'fsmt-': 'mount_target_id', - 'subnet-': 'subnet_id', - 'eni-': 'network_interface_id', - 'sg-': 'security_groups' - } - return first_or_default([attr_name for (prefix, attr_name) in attr_by_prefix.items() - if str(attr_id).startswith(prefix)], 'ip_address') - - -def first_or_default(items, default=None): - """ - Helper method to fetch first element of list (if exists) - """ - for item in items: - return item - return default - - -def has_tags(available, required): - """ - Helper method to determine if tag requested already exists - """ - for key, value in required.items(): - if key not in available or value != available[key]: - return False - return True - - -def has_targets(available, required): - """ - Helper method to determine if mount target requested already exists - """ - grouped = group_list_of_dict(available) - for (value, field) in required: - if field not in grouped or value not in grouped[field]: - return False - return True - - -def group_list_of_dict(array): - """ - Helper method to group list of dict to dict with all possible values - """ - result = defaultdict(list) - for item in array: - for key, value in item.items(): - result[key] += value if isinstance(value, list) else [value] - return result - - -def main(): - """ - Module action handler - """ - argument_spec = dict( - id=dict(), - name=dict(aliases=['creation_token']), - tags=dict(type="dict", default={}), - targets=dict(type="list", default=[]) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True) - is_old_facts = module._name == 'efs_facts' - if is_old_facts: - module.deprecate("The 'efs_facts' module has been renamed to 'efs_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - connection = EFSConnection(module) - - name = module.params.get('name') - fs_id = module.params.get('id') - tags = module.params.get('tags') - targets = module.params.get('targets') - - file_systems_info = connection.get_file_systems(fs_id, name) - - if tags: - file_systems_info = [item for item in file_systems_info if has_tags(item['tags'], tags)] - - file_systems_info = connection.get_mount_targets_data(file_systems_info) - file_systems_info = connection.get_security_groups_data(file_systems_info) - - if targets: - targets = [(item, prefix_to_attr(item)) for item in targets] - file_systems_info = [item for item in file_systems_info if has_targets(item['mount_targets'], targets)] - - if is_old_facts: - module.exit_json(changed=False, ansible_facts={'efs': file_systems_info}) - else: - module.exit_json(changed=False, efs=file_systems_info) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elasticache.py b/lib/ansible/modules/cloud/amazon/elasticache.py deleted file mode 100644 index 080fc77c7f..0000000000 --- a/lib/ansible/modules/cloud/amazon/elasticache.py +++ /dev/null @@ -1,562 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: elasticache -short_description: Manage cache clusters in Amazon ElastiCache -description: - - Manage cache clusters in Amazon ElastiCache. - - Returns information about the specified cache cluster. -version_added: "1.4" -requirements: [ boto3 ] -author: "Jim Dalton (@jsdalton)" -options: - state: - description: - - C(absent) or C(present) are idempotent actions that will create or destroy a cache cluster as needed. - - C(rebooted) will reboot the cluster, resulting in a momentary outage. - choices: ['present', 'absent', 'rebooted'] - required: true - type: str - name: - description: - - The cache cluster identifier. - required: true - type: str - engine: - description: - - Name of the cache engine to be used. - - Supported values are C(redis) and C(memcached). - default: memcached - type: str - cache_engine_version: - description: - - The version number of the cache engine. - type: str - node_type: - description: - - The compute and memory capacity of the nodes in the cache cluster. - default: cache.t2.small - type: str - num_nodes: - description: - - The initial number of cache nodes that the cache cluster will have. - - Required when I(state=present). - type: int - default: 1 - cache_port: - description: - - The port number on which each of the cache nodes will accept - connections. - type: int - cache_parameter_group: - description: - - The name of the cache parameter group to associate with this cache cluster. If this argument is omitted, the default cache parameter group - for the specified engine will be used. - version_added: "2.0" - aliases: [ 'parameter_group' ] - type: str - cache_subnet_group: - description: - - The subnet group name to associate with. Only use if inside a vpc. - - Required if inside a vpc - version_added: "2.0" - type: str - security_group_ids: - description: - - A list of vpc security group IDs to associate with this cache cluster. Only use if inside a vpc. - type: list - elements: str - version_added: "1.6" - cache_security_groups: - description: - - A list of cache security group names to associate with this cache cluster. Must be an empty list if inside a vpc. - type: list - elements: str - zone: - description: - - The EC2 Availability Zone in which the cache cluster will be created. - type: str - wait: - description: - - Wait for cache cluster result before returning. - type: bool - default: true - hard_modify: - description: - - Whether to destroy and recreate an existing cache cluster if necessary in order to modify its state. - type: bool - default: false -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. - -# Basic example -- elasticache: - name: "test-please-delete" - state: present - engine: memcached - cache_engine_version: 1.4.14 - node_type: cache.m1.small - num_nodes: 1 - cache_port: 11211 - cache_security_groups: - - default - zone: us-east-1d - - -# Ensure cache cluster is gone -- elasticache: - name: "test-please-delete" - state: absent - -# Reboot cache cluster -- elasticache: - name: "test-please-delete" - state: rebooted - -""" -from time import sleep -from traceback import format_exc -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn, HAS_BOTO3, camel_dict_to_snake_dict - -try: - import boto3 - import botocore -except ImportError: - pass # will be detected by imported HAS_BOTO3 - - -class ElastiCacheManager(object): - - """Handles elasticache creation and destruction""" - - EXIST_STATUSES = ['available', 'creating', 'rebooting', 'modifying'] - - def __init__(self, module, name, engine, cache_engine_version, node_type, - num_nodes, cache_port, cache_parameter_group, cache_subnet_group, - cache_security_groups, security_group_ids, zone, wait, - hard_modify, region, **aws_connect_kwargs): - self.module = module - self.name = name - self.engine = engine.lower() - self.cache_engine_version = cache_engine_version - self.node_type = node_type - self.num_nodes = num_nodes - self.cache_port = cache_port - self.cache_parameter_group = cache_parameter_group - self.cache_subnet_group = cache_subnet_group - self.cache_security_groups = cache_security_groups - self.security_group_ids = security_group_ids - self.zone = zone - self.wait = wait - self.hard_modify = hard_modify - - self.region = region - self.aws_connect_kwargs = aws_connect_kwargs - - self.changed = False - self.data = None - self.status = 'gone' - self.conn = self._get_elasticache_connection() - self._refresh_data() - - def ensure_present(self): - """Ensure cache cluster exists or create it if not""" - if self.exists(): - self.sync() - else: - self.create() - - def ensure_absent(self): - """Ensure cache cluster is gone or delete it if not""" - self.delete() - - def ensure_rebooted(self): - """Ensure cache cluster is gone or delete it if not""" - self.reboot() - - def exists(self): - """Check if cache cluster exists""" - return self.status in self.EXIST_STATUSES - - def create(self): - """Create an ElastiCache cluster""" - if self.status == 'available': - return - if self.status in ['creating', 'rebooting', 'modifying']: - if self.wait: - self._wait_for_status('available') - return - if self.status == 'deleting': - if self.wait: - self._wait_for_status('gone') - else: - msg = "'%s' is currently deleting. Cannot create." - self.module.fail_json(msg=msg % self.name) - - kwargs = dict(CacheClusterId=self.name, - NumCacheNodes=self.num_nodes, - CacheNodeType=self.node_type, - Engine=self.engine, - EngineVersion=self.cache_engine_version, - CacheSecurityGroupNames=self.cache_security_groups, - SecurityGroupIds=self.security_group_ids, - CacheParameterGroupName=self.cache_parameter_group, - CacheSubnetGroupName=self.cache_subnet_group) - if self.cache_port is not None: - kwargs['Port'] = self.cache_port - if self.zone is not None: - kwargs['PreferredAvailabilityZone'] = self.zone - - try: - self.conn.create_cache_cluster(**kwargs) - - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg=e.message, exception=format_exc(), - **camel_dict_to_snake_dict(e.response)) - - self._refresh_data() - - self.changed = True - if self.wait: - self._wait_for_status('available') - return True - - def delete(self): - """Destroy an ElastiCache cluster""" - if self.status == 'gone': - return - if self.status == 'deleting': - if self.wait: - self._wait_for_status('gone') - return - if self.status in ['creating', 'rebooting', 'modifying']: - if self.wait: - self._wait_for_status('available') - else: - msg = "'%s' is currently %s. Cannot delete." - self.module.fail_json(msg=msg % (self.name, self.status)) - - try: - response = self.conn.delete_cache_cluster(CacheClusterId=self.name) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg=e.message, exception=format_exc(), - **camel_dict_to_snake_dict(e.response)) - - cache_cluster_data = response['CacheCluster'] - self._refresh_data(cache_cluster_data) - - self.changed = True - if self.wait: - self._wait_for_status('gone') - - def sync(self): - """Sync settings to cluster if required""" - if not self.exists(): - msg = "'%s' is %s. Cannot sync." - self.module.fail_json(msg=msg % (self.name, self.status)) - - if self.status in ['creating', 'rebooting', 'modifying']: - if self.wait: - self._wait_for_status('available') - else: - # Cluster can only be synced if available. If we can't wait - # for this, then just be done. - return - - if self._requires_destroy_and_create(): - if not self.hard_modify: - msg = "'%s' requires destructive modification. 'hard_modify' must be set to true to proceed." - self.module.fail_json(msg=msg % self.name) - if not self.wait: - msg = "'%s' requires destructive modification. 'wait' must be set to true." - self.module.fail_json(msg=msg % self.name) - self.delete() - self.create() - return - - if self._requires_modification(): - self.modify() - - def modify(self): - """Modify the cache cluster. Note it's only possible to modify a few select options.""" - nodes_to_remove = self._get_nodes_to_remove() - try: - self.conn.modify_cache_cluster(CacheClusterId=self.name, - NumCacheNodes=self.num_nodes, - CacheNodeIdsToRemove=nodes_to_remove, - CacheSecurityGroupNames=self.cache_security_groups, - CacheParameterGroupName=self.cache_parameter_group, - SecurityGroupIds=self.security_group_ids, - ApplyImmediately=True, - EngineVersion=self.cache_engine_version) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg=e.message, exception=format_exc(), - **camel_dict_to_snake_dict(e.response)) - - self._refresh_data() - - self.changed = True - if self.wait: - self._wait_for_status('available') - - def reboot(self): - """Reboot the cache cluster""" - if not self.exists(): - msg = "'%s' is %s. Cannot reboot." - self.module.fail_json(msg=msg % (self.name, self.status)) - if self.status == 'rebooting': - return - if self.status in ['creating', 'modifying']: - if self.wait: - self._wait_for_status('available') - else: - msg = "'%s' is currently %s. Cannot reboot." - self.module.fail_json(msg=msg % (self.name, self.status)) - - # Collect ALL nodes for reboot - cache_node_ids = [cn['CacheNodeId'] for cn in self.data['CacheNodes']] - try: - self.conn.reboot_cache_cluster(CacheClusterId=self.name, - CacheNodeIdsToReboot=cache_node_ids) - except botocore.exceptions.ClientError as e: - self.module.fail_json(msg=e.message, exception=format_exc(), - **camel_dict_to_snake_dict(e.response)) - - self._refresh_data() - - self.changed = True - if self.wait: - self._wait_for_status('available') - - def get_info(self): - """Return basic info about the cache cluster""" - info = { - 'name': self.name, - 'status': self.status - } - if self.data: - info['data'] = self.data - return info - - def _wait_for_status(self, awaited_status): - """Wait for status to change from present status to awaited_status""" - status_map = { - 'creating': 'available', - 'rebooting': 'available', - 'modifying': 'available', - 'deleting': 'gone' - } - if self.status == awaited_status: - # No need to wait, we're already done - return - if status_map[self.status] != awaited_status: - msg = "Invalid awaited status. '%s' cannot transition to '%s'" - self.module.fail_json(msg=msg % (self.status, awaited_status)) - - if awaited_status not in set(status_map.values()): - msg = "'%s' is not a valid awaited status." - self.module.fail_json(msg=msg % awaited_status) - - while True: - sleep(1) - self._refresh_data() - if self.status == awaited_status: - break - - def _requires_modification(self): - """Check if cluster requires (nondestructive) modification""" - # Check modifiable data attributes - modifiable_data = { - 'NumCacheNodes': self.num_nodes, - 'EngineVersion': self.cache_engine_version - } - for key, value in modifiable_data.items(): - if value is not None and value and self.data[key] != value: - return True - - # Check cache security groups - cache_security_groups = [] - for sg in self.data['CacheSecurityGroups']: - cache_security_groups.append(sg['CacheSecurityGroupName']) - if set(cache_security_groups) != set(self.cache_security_groups): - return True - - # check vpc security groups - if self.security_group_ids: - vpc_security_groups = [] - security_groups = self.data['SecurityGroups'] or [] - for sg in security_groups: - vpc_security_groups.append(sg['SecurityGroupId']) - if set(vpc_security_groups) != set(self.security_group_ids): - return True - - return False - - def _requires_destroy_and_create(self): - """ - Check whether a destroy and create is required to synchronize cluster. - """ - unmodifiable_data = { - 'node_type': self.data['CacheNodeType'], - 'engine': self.data['Engine'], - 'cache_port': self._get_port() - } - # Only check for modifications if zone is specified - if self.zone is not None: - unmodifiable_data['zone'] = self.data['PreferredAvailabilityZone'] - for key, value in unmodifiable_data.items(): - if getattr(self, key) is not None and getattr(self, key) != value: - return True - return False - - def _get_elasticache_connection(self): - """Get an elasticache connection""" - region, ec2_url, aws_connect_params = get_aws_connection_info(self.module, boto3=True) - if region: - return boto3_conn(self.module, conn_type='client', resource='elasticache', - region=region, endpoint=ec2_url, **aws_connect_params) - else: - self.module.fail_json(msg="region must be specified") - - def _get_port(self): - """Get the port. Where this information is retrieved from is engine dependent.""" - if self.data['Engine'] == 'memcached': - return self.data['ConfigurationEndpoint']['Port'] - elif self.data['Engine'] == 'redis': - # Redis only supports a single node (presently) so just use - # the first and only - return self.data['CacheNodes'][0]['Endpoint']['Port'] - - def _refresh_data(self, cache_cluster_data=None): - """Refresh data about this cache cluster""" - - if cache_cluster_data is None: - try: - response = self.conn.describe_cache_clusters(CacheClusterId=self.name, ShowCacheNodeInfo=True) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'CacheClusterNotFound': - self.data = None - self.status = 'gone' - return - else: - self.module.fail_json(msg=e.message, exception=format_exc(), - **camel_dict_to_snake_dict(e.response)) - cache_cluster_data = response['CacheClusters'][0] - self.data = cache_cluster_data - self.status = self.data['CacheClusterStatus'] - - # The documentation for elasticache lies -- status on rebooting is set - # to 'rebooting cache cluster nodes' instead of 'rebooting'. Fix it - # here to make status checks etc. more sane. - if self.status == 'rebooting cache cluster nodes': - self.status = 'rebooting' - - def _get_nodes_to_remove(self): - """If there are nodes to remove, it figures out which need to be removed""" - num_nodes_to_remove = self.data['NumCacheNodes'] - self.num_nodes - if num_nodes_to_remove <= 0: - return [] - - if not self.hard_modify: - msg = "'%s' requires removal of cache nodes. 'hard_modify' must be set to true to proceed." - self.module.fail_json(msg=msg % self.name) - - cache_node_ids = [cn['CacheNodeId'] for cn in self.data['CacheNodes']] - return cache_node_ids[-num_nodes_to_remove:] - - -def main(): - """ elasticache ansible module """ - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent', 'rebooted']), - name=dict(required=True), - engine=dict(default='memcached'), - cache_engine_version=dict(default=""), - node_type=dict(default='cache.t2.small'), - num_nodes=dict(default=1, type='int'), - # alias for compat with the original PR 1950 - cache_parameter_group=dict(default="", aliases=['parameter_group']), - cache_port=dict(type='int'), - cache_subnet_group=dict(default=""), - cache_security_groups=dict(default=[], type='list'), - security_group_ids=dict(default=[], type='list'), - zone=dict(), - wait=dict(default=True, type='bool'), - hard_modify=dict(type='bool') - )) - - module = AnsibleModule( - argument_spec=argument_spec, - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - name = module.params['name'] - state = module.params['state'] - engine = module.params['engine'] - cache_engine_version = module.params['cache_engine_version'] - node_type = module.params['node_type'] - num_nodes = module.params['num_nodes'] - cache_port = module.params['cache_port'] - cache_subnet_group = module.params['cache_subnet_group'] - cache_security_groups = module.params['cache_security_groups'] - security_group_ids = module.params['security_group_ids'] - zone = module.params['zone'] - wait = module.params['wait'] - hard_modify = module.params['hard_modify'] - cache_parameter_group = module.params['cache_parameter_group'] - - if cache_subnet_group and cache_security_groups: - module.fail_json(msg="Can't specify both cache_subnet_group and cache_security_groups") - - if state == 'present' and not num_nodes: - module.fail_json(msg="'num_nodes' is a required parameter. Please specify num_nodes > 0") - - elasticache_manager = ElastiCacheManager(module, name, engine, - cache_engine_version, node_type, - num_nodes, cache_port, - cache_parameter_group, - cache_subnet_group, - cache_security_groups, - security_group_ids, zone, wait, - hard_modify, region, **aws_connect_kwargs) - - if state == 'present': - elasticache_manager.ensure_present() - elif state == 'absent': - elasticache_manager.ensure_absent() - elif state == 'rebooted': - elasticache_manager.ensure_rebooted() - - facts_result = dict(changed=elasticache_manager.changed, - elasticache=elasticache_manager.get_info()) - - module.exit_json(**facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elasticache_info.py b/lib/ansible/modules/cloud/amazon/elasticache_info.py deleted file mode 100644 index cd4a243810..0000000000 --- a/lib/ansible/modules/cloud/amazon/elasticache_info.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' -module: elasticache_info -short_description: Retrieve information for AWS ElastiCache clusters -description: - - Retrieve information from AWS ElastiCache clusters - - This module was called C(elasticache_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -options: - name: - description: - - The name of an ElastiCache cluster. - type: str - -author: - - Will Thames (@willthames) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: obtain all ElastiCache information - elasticache_info: - -- name: obtain all information for a single ElastiCache cluster - elasticache_info: - name: test_elasticache -''' - -RETURN = ''' -elasticache_clusters: - description: List of ElastiCache clusters - returned: always - type: complex - contains: - auto_minor_version_upgrade: - description: Whether to automatically upgrade to minor versions - returned: always - type: bool - sample: true - cache_cluster_create_time: - description: Date and time cluster was created - returned: always - type: str - sample: '2017-09-15T05:43:46.038000+00:00' - cache_cluster_id: - description: ID of the cache cluster - returned: always - type: str - sample: abcd-1234-001 - cache_cluster_status: - description: Status of ElastiCache cluster - returned: always - type: str - sample: available - cache_node_type: - description: Instance type of ElastiCache nodes - returned: always - type: str - sample: cache.t2.micro - cache_nodes: - description: List of ElastiCache nodes in the cluster - returned: always - type: complex - contains: - cache_node_create_time: - description: Date and time node was created - returned: always - type: str - sample: '2017-09-15T05:43:46.038000+00:00' - cache_node_id: - description: ID of the cache node - returned: always - type: str - sample: '0001' - cache_node_status: - description: Status of the cache node - returned: always - type: str - sample: available - customer_availability_zone: - description: Availability Zone in which the cache node was created - returned: always - type: str - sample: ap-southeast-2b - endpoint: - description: Connection details for the cache node - returned: always - type: complex - contains: - address: - description: URL of the cache node endpoint - returned: always - type: str - sample: abcd-1234-001.bgiz2p.0001.apse2.cache.amazonaws.com - port: - description: Port of the cache node endpoint - returned: always - type: int - sample: 6379 - parameter_group_status: - description: Status of the Cache Parameter Group - returned: always - type: str - sample: in-sync - cache_parameter_group: - description: Contents of the Cache Parameter Group - returned: always - type: complex - contains: - cache_node_ids_to_reboot: - description: Cache nodes which need to be rebooted for parameter changes to be applied - returned: always - type: list - sample: [] - cache_parameter_group_name: - description: Name of the cache parameter group - returned: always - type: str - sample: default.redis3.2 - parameter_apply_status: - description: Status of parameter updates - returned: always - type: str - sample: in-sync - cache_security_groups: - description: Security Groups used by the cache - returned: always - type: list - sample: - - 'sg-abcd1234' - cache_subnet_group_name: - description: ElastiCache Subnet Group used by the cache - returned: always - type: str - sample: abcd-subnet-group - client_download_landing_page: - description: URL of client download web page - returned: always - type: str - sample: 'https://console.aws.amazon.com/elasticache/home#client-download:' - engine: - description: Engine used by ElastiCache - returned: always - type: str - sample: redis - engine_version: - description: Version of ElastiCache engine - returned: always - type: str - sample: 3.2.4 - notification_configuration: - description: Configuration of notifications - returned: if notifications are enabled - type: complex - contains: - topic_arn: - description: ARN of notification destination topic - returned: if notifications are enabled - type: str - sample: arn:aws:sns:*:123456789012:my_topic - topic_name: - description: Name of notification destination topic - returned: if notifications are enabled - type: str - sample: MyTopic - num_cache_nodes: - description: Number of Cache Nodes - returned: always - type: int - sample: 1 - pending_modified_values: - description: Values that are pending modification - returned: always - type: complex - contains: {} - preferred_availability_zone: - description: Preferred Availability Zone - returned: always - type: str - sample: ap-southeast-2b - preferred_maintenance_window: - description: Time slot for preferred maintenance window - returned: always - type: str - sample: sat:12:00-sat:13:00 - replication_group_id: - description: Replication Group Id - returned: always - type: str - sample: replication-001 - security_groups: - description: List of Security Groups associated with ElastiCache - returned: always - type: complex - contains: - security_group_id: - description: Security Group ID - returned: always - type: str - sample: sg-abcd1234 - status: - description: Status of Security Group - returned: always - type: str - sample: active - tags: - description: Tags applied to the ElastiCache cluster - returned: always - type: complex - contains: {} - sample: - Application: web - Environment: test -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import get_aws_connection_info, camel_dict_to_snake_dict, AWSRetry, boto3_tag_list_to_ansible_dict - - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.exponential_backoff() -def describe_cache_clusters_with_backoff(client, cluster_id=None): - paginator = client.get_paginator('describe_cache_clusters') - params = dict(ShowCacheNodeInfo=True) - if cluster_id: - params['CacheClusterId'] = cluster_id - try: - response = paginator.paginate(**params).build_full_result() - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'CacheClusterNotFound': - return [] - raise - except botocore.exceptions.BotoCoreError: - raise - return response['CacheClusters'] - - -@AWSRetry.exponential_backoff() -def get_elasticache_tags_with_backoff(client, cluster_id): - return client.list_tags_for_resource(ResourceName=cluster_id)['TagList'] - - -def get_aws_account_id(module): - try: - client = module.client('sts') - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Can't authorize connection") - - try: - return client.get_caller_identity()['Account'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain AWS account id") - - -def get_elasticache_clusters(client, module): - region = get_aws_connection_info(module, boto3=True)[0] - try: - clusters = describe_cache_clusters_with_backoff(client, cluster_id=module.params.get('name')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain cache cluster info") - - account_id = get_aws_account_id(module) - results = [] - for cluster in clusters: - - cluster = camel_dict_to_snake_dict(cluster) - arn = "arn:aws:elasticache:%s:%s:cluster:%s" % (region, account_id, cluster['cache_cluster_id']) - try: - tags = get_elasticache_tags_with_backoff(client, arn) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get tags for cluster %s") - - cluster['tags'] = boto3_tag_list_to_ansible_dict(tags) - results.append(cluster) - return results - - -def main(): - argument_spec = dict( - name=dict(required=False), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'elasticache_facts': - module.deprecate("The 'elasticache_facts' module has been renamed to 'elasticache_info'", version='2.13') - - client = module.client('elasticache') - - module.exit_json(elasticache_clusters=get_elasticache_clusters(client, module)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py b/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py deleted file mode 100644 index 50951d24a0..0000000000 --- a/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: elasticache_parameter_group -short_description: Manage cache parameter groups in Amazon ElastiCache. -description: - - Manage cache security groups in Amazon ElastiCache. - - Returns information about the specified cache cluster. -version_added: "2.3" -author: "Sloane Hertel (@s-hertel)" -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3, botocore ] -options: - group_family: - description: - - The name of the cache parameter group family that the cache parameter group can be used with. - Required when creating a cache parameter group. - choices: ['memcached1.4', 'memcached1.5', 'redis2.6', 'redis2.8', 'redis3.2', 'redis4.0', 'redis5.0'] - type: str - name: - description: - - A user-specified name for the cache parameter group. - required: yes - type: str - description: - description: - - A user-specified description for the cache parameter group. - type: str - state: - description: - - Idempotent actions that will create/modify, destroy, or reset a cache parameter group as needed. - choices: ['present', 'absent', 'reset'] - required: true - type: str - values: - description: - - A user-specified dictionary of parameters to reset or modify for the cache parameter group. - type: dict -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. ---- -- hosts: localhost - connection: local - tasks: - - name: 'Create a test parameter group' - elasticache_parameter_group: - name: 'test-param-group' - group_family: 'redis3.2' - description: 'This is a cache parameter group' - state: 'present' - - name: 'Modify a test parameter group' - elasticache_parameter_group: - name: 'test-param-group' - values: - activerehashing: yes - client-output-buffer-limit-normal-hard-limit: 4 - state: 'present' - - name: 'Reset all modifiable parameters for the test parameter group' - elasticache_parameter_group: - name: 'test-param-group' - state: reset - - name: 'Delete a test parameter group' - elasticache_parameter_group: - name: 'test-param-group' - state: 'absent' -""" - -RETURN = """ -elasticache: - description: cache parameter group information and response metadata - returned: always - type: dict - sample: - cache_parameter_group: - cache_parameter_group_family: redis3.2 - cache_parameter_group_name: test-please-delete - description: "initial description" - response_metadata: - http_headers: - content-length: "562" - content-type: text/xml - date: "Mon, 06 Feb 2017 22:14:08 GMT" - x-amzn-requestid: 947291f9-ecb9-11e6-85bd-3baa4eca2cc1 - http_status_code: 200 - request_id: 947291f9-ecb9-11e6-85bd-3baa4eca2cc1 - retry_attempts: 0 -changed: - description: if the cache parameter group has changed - returned: always - type: bool - sample: - changed: true -""" - -# import module snippets -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict -from ansible.module_utils._text import to_text -from ansible.module_utils.six import string_types -import traceback - -try: - import boto3 - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - - -def create(module, conn, name, group_family, description): - """ Create ElastiCache parameter group. """ - try: - response = conn.create_cache_parameter_group(CacheParameterGroupName=name, CacheParameterGroupFamily=group_family, Description=description) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to create cache parameter group.", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - return response, changed - - -def delete(module, conn, name): - """ Delete ElastiCache parameter group. """ - try: - conn.delete_cache_parameter_group(CacheParameterGroupName=name) - response = {} - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to delete cache parameter group.", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - return response, changed - - -def make_current_modifiable_param_dict(module, conn, name): - """ Gets the current state of the cache parameter group and creates a dict with the format: {ParameterName: [Allowed_Values, DataType, ParameterValue]}""" - current_info = get_info(conn, name) - if current_info is False: - module.fail_json(msg="Could not connect to the cache parameter group %s." % name) - - parameters = current_info["Parameters"] - modifiable_params = {} - - for param in parameters: - if param["IsModifiable"]: - modifiable_params[param["ParameterName"]] = [param.get("AllowedValues")] - modifiable_params[param["ParameterName"]].append(param["DataType"]) - modifiable_params[param["ParameterName"]].append(param.get("ParameterValue")) - return modifiable_params - - -def check_valid_modification(module, values, modifiable_params): - """ Check if the parameters and values in values are valid. """ - changed_with_update = False - - for parameter in values: - new_value = values[parameter] - - # check valid modifiable parameters - if parameter not in modifiable_params: - module.fail_json(msg="%s is not a modifiable parameter. Valid parameters to modify are: %s." % (parameter, modifiable_params.keys())) - - # check allowed datatype for modified parameters - str_to_type = {"integer": int, "string": string_types} - expected_type = str_to_type[modifiable_params[parameter][1]] - if not isinstance(new_value, expected_type): - if expected_type == str: - if isinstance(new_value, bool): - values[parameter] = "yes" if new_value else "no" - else: - values[parameter] = to_text(new_value) - elif expected_type == int: - if isinstance(new_value, bool): - values[parameter] = 1 if new_value else 0 - else: - module.fail_json(msg="%s (type %s) is not an allowed value for the parameter %s. Expected a type %s." % - (new_value, type(new_value), parameter, modifiable_params[parameter][1])) - else: - module.fail_json(msg="%s (type %s) is not an allowed value for the parameter %s. Expected a type %s." % - (new_value, type(new_value), parameter, modifiable_params[parameter][1])) - - # check allowed values for modifiable parameters - choices = modifiable_params[parameter][0] - if choices: - if not (to_text(new_value) in choices or isinstance(new_value, int)): - module.fail_json(msg="%s is not an allowed value for the parameter %s. Valid parameters are: %s." % - (new_value, parameter, choices)) - - # check if a new value is different from current value - if to_text(values[parameter]) != modifiable_params[parameter][2]: - changed_with_update = True - - return changed_with_update, values - - -def check_changed_parameter_values(values, old_parameters, new_parameters): - """ Checking if the new values are different than the old values. """ - changed_with_update = False - - # if the user specified parameters to reset, only check those for change - if values: - for parameter in values: - if old_parameters[parameter] != new_parameters[parameter]: - changed_with_update = True - break - # otherwise check all to find a change - else: - for parameter in old_parameters: - if old_parameters[parameter] != new_parameters[parameter]: - changed_with_update = True - break - - return changed_with_update - - -def modify(module, conn, name, values): - """ Modify ElastiCache parameter group to reflect the new information if it differs from the current. """ - # compares current group parameters with the parameters we've specified to to a value to see if this will change the group - format_parameters = [] - for key in values: - value = to_text(values[key]) - format_parameters.append({'ParameterName': key, 'ParameterValue': value}) - try: - response = conn.modify_cache_parameter_group(CacheParameterGroupName=name, ParameterNameValues=format_parameters) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to modify cache parameter group.", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - return response - - -def reset(module, conn, name, values): - """ Reset ElastiCache parameter group if the current information is different from the new information. """ - # used to compare with the reset parameters' dict to see if there have been changes - old_parameters_dict = make_current_modifiable_param_dict(module, conn, name) - - format_parameters = [] - - # determine whether to reset all or specific parameters - if values: - all_parameters = False - format_parameters = [] - for key in values: - value = to_text(values[key]) - format_parameters.append({'ParameterName': key, 'ParameterValue': value}) - else: - all_parameters = True - - try: - response = conn.reset_cache_parameter_group(CacheParameterGroupName=name, ParameterNameValues=format_parameters, ResetAllParameters=all_parameters) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to reset cache parameter group.", exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - # determine changed - new_parameters_dict = make_current_modifiable_param_dict(module, conn, name) - changed = check_changed_parameter_values(values, old_parameters_dict, new_parameters_dict) - - return response, changed - - -def get_info(conn, name): - """ Gets info about the ElastiCache parameter group. Returns false if it doesn't exist or we don't have access. """ - try: - data = conn.describe_cache_parameters(CacheParameterGroupName=name) - return data - except botocore.exceptions.ClientError as e: - return False - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - group_family=dict(type='str', choices=['memcached1.4', 'memcached1.5', 'redis2.6', 'redis2.8', 'redis3.2', 'redis4.0', 'redis5.0']), - name=dict(required=True, type='str'), - description=dict(default='', type='str'), - state=dict(required=True, choices=['present', 'absent', 'reset']), - values=dict(type='dict'), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO3: - module.fail_json(msg='boto required for this module') - - parameter_group_family = module.params.get('group_family') - parameter_group_name = module.params.get('name') - group_description = module.params.get('description') - state = module.params.get('state') - values = module.params.get('values') - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.") - - connection = boto3_conn(module, conn_type='client', - resource='elasticache', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - - exists = get_info(connection, parameter_group_name) - - # check that the needed requirements are available - if state == 'present' and not (exists or parameter_group_family): - module.fail_json(msg="Creating a group requires a family group.") - elif state == 'reset' and not exists: - module.fail_json(msg="No group %s to reset. Please create the group before using the state 'reset'." % parameter_group_name) - - # Taking action - changed = False - if state == 'present': - if exists: - # confirm that the group exists without any actions - if not values: - response = exists - changed = False - # modify existing group - else: - modifiable_params = make_current_modifiable_param_dict(module, connection, parameter_group_name) - changed, values = check_valid_modification(module, values, modifiable_params) - response = modify(module, connection, parameter_group_name, values) - # create group - else: - response, changed = create(module, connection, parameter_group_name, parameter_group_family, group_description) - if values: - modifiable_params = make_current_modifiable_param_dict(module, connection, parameter_group_name) - changed, values = check_valid_modification(module, values, modifiable_params) - response = modify(module, connection, parameter_group_name, values) - elif state == 'absent': - if exists: - # delete group - response, changed = delete(module, connection, parameter_group_name) - else: - response = {} - changed = False - elif state == 'reset': - response, changed = reset(module, connection, parameter_group_name, values) - - facts_result = dict(changed=changed, elasticache=camel_dict_to_snake_dict(response)) - - module.exit_json(**facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elasticache_snapshot.py b/lib/ansible/modules/cloud/amazon/elasticache_snapshot.py deleted file mode 100644 index 1883499394..0000000000 --- a/lib/ansible/modules/cloud/amazon/elasticache_snapshot.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: elasticache_snapshot -short_description: Manage cache snapshots in Amazon ElastiCache -description: - - Manage cache snapshots in Amazon ElastiCache. - - Returns information about the specified snapshot. -version_added: "2.3" -author: "Sloane Hertel (@s-hertel)" -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3, botocore ] -options: - name: - description: - - The name of the snapshot we want to create, copy, delete. - required: true - type: str - state: - description: - - Actions that will create, destroy, or copy a snapshot. - required: true - choices: ['present', 'absent', 'copy'] - type: str - replication_id: - description: - - The name of the existing replication group to make the snapshot. - type: str - cluster_id: - description: - - The name of an existing cache cluster in the replication group to make the snapshot. - type: str - target: - description: - - The name of a snapshot copy. - type: str - bucket: - description: - - The s3 bucket to which the snapshot is exported. - type: str -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. ---- -- hosts: localhost - connection: local - tasks: - - name: 'Create a snapshot' - elasticache_snapshot: - name: 'test-snapshot' - state: 'present' - cluster_id: '{{ cluster }}' - replication_id: '{{ replication }}' -""" - -RETURN = """ -response_metadata: - description: response metadata about the snapshot - returned: always - type: dict - sample: - http_headers: - content-length: 1490 - content-type: text/xml - date: Tue, 07 Feb 2017 16:43:04 GMT - x-amzn-requestid: 7f436dea-ed54-11e6-a04c-ab2372a1f14d - http_status_code: 200 - request_id: 7f436dea-ed54-11e6-a04c-ab2372a1f14d - retry_attempts: 0 -snapshot: - description: snapshot data - returned: always - type: dict - sample: - auto_minor_version_upgrade: true - cache_cluster_create_time: 2017-02-01T17:43:58.261000+00:00 - cache_cluster_id: test-please-delete - cache_node_type: cache.m1.small - cache_parameter_group_name: default.redis3.2 - cache_subnet_group_name: default - engine: redis - engine_version: 3.2.4 - node_snapshots: - cache_node_create_time: 2017-02-01T17:43:58.261000+00:00 - cache_node_id: 0001 - cache_size: - num_cache_nodes: 1 - port: 11211 - preferred_availability_zone: us-east-1d - preferred_maintenance_window: wed:03:00-wed:04:00 - snapshot_name: deletesnapshot - snapshot_retention_limit: 0 - snapshot_source: manual - snapshot_status: creating - snapshot_window: 10:00-11:00 - vpc_id: vpc-c248fda4 -changed: - description: if a snapshot has been created, deleted, or copied - returned: always - type: bool - sample: - changed: true -""" - -import traceback - -try: - import boto3 - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict - - -def create(module, connection, replication_id, cluster_id, name): - """ Create an ElastiCache backup. """ - try: - response = connection.create_snapshot(ReplicationGroupId=replication_id, - CacheClusterId=cluster_id, - SnapshotName=name) - changed = True - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == "SnapshotAlreadyExistsFault": - response = {} - changed = False - else: - module.fail_json(msg="Unable to create the snapshot.", exception=traceback.format_exc()) - return response, changed - - -def copy(module, connection, name, target, bucket): - """ Copy an ElastiCache backup. """ - try: - response = connection.copy_snapshot(SourceSnapshotName=name, - TargetSnapshotName=target, - TargetBucket=bucket) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to copy the snapshot.", exception=traceback.format_exc()) - return response, changed - - -def delete(module, connection, name): - """ Delete an ElastiCache backup. """ - try: - response = connection.delete_snapshot(SnapshotName=name) - changed = True - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == "SnapshotNotFoundFault": - response = {} - changed = False - elif e.response['Error']['Code'] == "InvalidSnapshotState": - module.fail_json(msg="Error: InvalidSnapshotState. The snapshot is not in an available state or failed state to allow deletion." - "You may need to wait a few minutes.") - else: - module.fail_json(msg="Unable to delete the snapshot.", exception=traceback.format_exc()) - return response, changed - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True, type='str'), - state=dict(required=True, type='str', choices=['present', 'absent', 'copy']), - replication_id=dict(type='str'), - cluster_id=dict(type='str'), - target=dict(type='str'), - bucket=dict(type='str'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO3: - module.fail_json(msg='boto required for this module') - - name = module.params.get('name') - state = module.params.get('state') - replication_id = module.params.get('replication_id') - cluster_id = module.params.get('cluster_id') - target = module.params.get('target') - bucket = module.params.get('bucket') - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg=str("Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.")) - - connection = boto3_conn(module, conn_type='client', - resource='elasticache', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - - changed = False - response = {} - - if state == 'present': - if not all((replication_id, cluster_id)): - module.fail_json(msg="The state 'present' requires options: 'replication_id' and 'cluster_id'") - response, changed = create(module, connection, replication_id, cluster_id, name) - elif state == 'absent': - response, changed = delete(module, connection, name) - elif state == 'copy': - if not all((target, bucket)): - module.fail_json(msg="The state 'copy' requires options: 'target' and 'bucket'.") - response, changed = copy(module, connection, name, target, bucket) - - facts_result = dict(changed=changed, **camel_dict_to_snake_dict(response)) - - module.exit_json(**facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py b/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py deleted file mode 100644 index e1425416b5..0000000000 --- a/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: elasticache_subnet_group -version_added: "2.0" -short_description: manage ElastiCache subnet groups -description: - - Creates, modifies, and deletes ElastiCache subnet groups. This module has a dependency on python-boto >= 2.5. -options: - state: - description: - - Specifies whether the subnet should be present or absent. - required: true - choices: [ 'present' , 'absent' ] - type: str - name: - description: - - Database subnet group identifier. - required: true - type: str - description: - description: - - ElastiCache subnet group description. Only set when a new group is added. - type: str - subnets: - description: - - List of subnet IDs that make up the ElastiCache subnet group. - type: list - elements: str -author: "Tim Mahoney (@timmahoney)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Add or change a subnet group -- elasticache_subnet_group: - state: present - name: norwegian-blue - description: My Fancy Ex Parrot Subnet Group - subnets: - - subnet-aaaaaaaa - - subnet-bbbbbbbb - -# Remove a subnet group -- elasticache_subnet_group: - state: absent - name: norwegian-blue -''' - -try: - import boto - from boto.elasticache import connect_to_region - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, ec2_argument_spec, get_aws_connection_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(required=True), - description=dict(required=False), - subnets=dict(required=False, type='list'), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - state = module.params.get('state') - group_name = module.params.get('name').lower() - group_description = module.params.get('description') - group_subnets = module.params.get('subnets') or {} - - if state == 'present': - for required in ['name', 'description', 'subnets']: - if not module.params.get(required): - module.fail_json(msg=str("Parameter %s required for state='present'" % required)) - else: - for not_allowed in ['description', 'subnets']: - if module.params.get(not_allowed): - module.fail_json(msg=str("Parameter %s not allowed for state='absent'" % not_allowed)) - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - if not region: - module.fail_json(msg=str("Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.")) - - """Get an elasticache connection""" - try: - conn = connect_to_region(region_name=region, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=e.message) - - try: - changed = False - exists = False - - try: - matching_groups = conn.describe_cache_subnet_groups(group_name, max_records=100) - exists = len(matching_groups) > 0 - except BotoServerError as e: - if e.error_code != 'CacheSubnetGroupNotFoundFault': - module.fail_json(msg=e.error_message) - - if state == 'absent': - if exists: - conn.delete_cache_subnet_group(group_name) - changed = True - else: - if not exists: - new_group = conn.create_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets) - changed = True - else: - changed_group = conn.modify_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets) - changed = True - - except BotoServerError as e: - if e.error_message != 'No modifications were requested.': - module.fail_json(msg=e.error_message) - else: - changed = False - - module.exit_json(changed=changed) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_application_lb.py b/lib/ansible/modules/cloud/amazon/elb_application_lb.py deleted file mode 100644 index a14e4f70f1..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_application_lb.py +++ /dev/null @@ -1,659 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: elb_application_lb -short_description: Manage an Application load balancer -description: - - Manage an AWS Application Elastic Load Balancer. See U(https://aws.amazon.com/blogs/aws/new-aws-application-load-balancer/) for details. -version_added: "2.4" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - access_logs_enabled: - description: - - Whether or not to enable access logs. - - When set, I(access_logs_s3_bucket) must also be set. - type: bool - access_logs_s3_bucket: - description: - - The name of the S3 bucket for the access logs. - - The bucket must exist in the same - region as the load balancer and have a bucket policy that grants Elastic Load Balancing permission to write to the bucket. - - Required if access logs in Amazon S3 are enabled. - - When set, I(access_logs_enabled) must also be set. - type: str - access_logs_s3_prefix: - description: - - The prefix for the log location in the S3 bucket. - - If you don't specify a prefix, the access logs are stored in the root of the bucket. - - Cannot begin or end with a slash. - type: str - deletion_protection: - description: - - Indicates whether deletion protection for the ELB is enabled. - default: no - type: bool - http2: - description: - - Indicates whether to enable HTTP2 routing. - default: no - type: bool - version_added: 2.6 - idle_timeout: - description: - - The number of seconds to wait before an idle connection is closed. - type: int - listeners: - description: - - A list of dicts containing listeners to attach to the ELB. See examples for detail of the dict required. Note that listener keys - are CamelCased. - type: list - suboptions: - Port: - description: The port on which the load balancer is listening. - required: true - type: int - Protocol: - description: The protocol for connections from clients to the load balancer. - required: true - type: str - Certificates: - description: The SSL server certificate. - type: list - suboptions: - CertificateArn: - description: The Amazon Resource Name (ARN) of the certificate. - type: str - SslPolicy: - description: The security policy that defines which ciphers and protocols are supported. - type: str - DefaultActions: - description: The default actions for the listener. - required: true - type: list - suboptions: - Type: - description: The type of action. - type: str - TargetGroupArn: - description: The Amazon Resource Name (ARN) of the target group. - type: str - Rules: - type: list - description: - - A list of ALB Listener Rules. - - 'For the complete documentation of possible Conditions and Actions please see the boto3 documentation:' - - 'https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_rule' - suboptions: - Conditions: - type: list - description: Conditions which must be met for the actions to be applied. - Priority: - type: int - description: The rule priority. - Actions: - type: list - description: Actions to apply if all of the rule's conditions are met. - name: - description: - - The name of the load balancer. This name must be unique within your AWS account, can have a maximum of 32 characters, must contain only alphanumeric - characters or hyphens, and must not begin or end with a hyphen. - required: true - type: str - purge_listeners: - description: - - If yes, existing listeners will be purged from the ELB to match exactly what is defined by I(listeners) parameter. If the I(listeners) parameter is - not set then listeners will not be modified - default: yes - type: bool - purge_tags: - description: - - If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. If the I(tags) parameter is not set then - tags will not be modified. - default: yes - type: bool - subnets: - description: - - A list of the IDs of the subnets to attach to the load balancer. You can specify only one subnet per Availability Zone. You must specify subnets from - at least two Availability Zones. - - Required if I(state=present). - type: list - security_groups: - description: - - A list of the names or IDs of the security groups to assign to the load balancer. - - Required if I(state=present). - default: [] - type: list - scheme: - description: - - Internet-facing or internal load balancer. An ELB scheme can not be modified after creation. - default: internet-facing - choices: [ 'internet-facing', 'internal' ] - type: str - state: - description: - - Create or destroy the load balancer. - default: present - choices: [ 'present', 'absent' ] - type: str - tags: - description: - - A dictionary of one or more tags to assign to the load balancer. - type: dict - wait: - description: - - Wait for the load balancer to have a state of 'active' before completing. A status check is - performed every 15 seconds until a successful state is reached. An error is returned after 40 failed checks. - default: no - type: bool - version_added: 2.6 - wait_timeout: - description: - - The time in seconds to use in conjunction with I(wait). - version_added: 2.6 - type: int - purge_rules: - description: - - When set to no, keep the existing load balancer rules in place. Will modify and add, but will not delete. - default: yes - type: bool - version_added: 2.7 -extends_documentation_fragment: - - aws - - ec2 -notes: - - Listeners are matched based on port. If a listener's port is changed then a new listener will be created. - - Listener rules are matched based on priority. If a rule's priority is changed then a new rule will be created. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an ELB and attach a listener -- elb_application_lb: - name: myelb - security_groups: - - sg-12345678 - - my-sec-group - subnets: - - subnet-012345678 - - subnet-abcdef000 - listeners: - - Protocol: HTTP # Required. The protocol for connections from clients to the load balancer (HTTP or HTTPS) (case-sensitive). - Port: 80 # Required. The port on which the load balancer is listening. - # The security policy that defines which ciphers and protocols are supported. The default is the current predefined security policy. - SslPolicy: ELBSecurityPolicy-2015-05 - Certificates: # The ARN of the certificate (only one certficate ARN should be provided) - - CertificateArn: arn:aws:iam::12345678987:server-certificate/test.domain.com - DefaultActions: - - Type: forward # Required. - TargetGroupName: # Required. The name of the target group - state: present - -# Create an ELB and attach a listener with logging enabled -- elb_application_lb: - access_logs_enabled: yes - access_logs_s3_bucket: mybucket - access_logs_s3_prefix: "logs" - name: myelb - security_groups: - - sg-12345678 - - my-sec-group - subnets: - - subnet-012345678 - - subnet-abcdef000 - listeners: - - Protocol: HTTP # Required. The protocol for connections from clients to the load balancer (HTTP or HTTPS) (case-sensitive). - Port: 80 # Required. The port on which the load balancer is listening. - # The security policy that defines which ciphers and protocols are supported. The default is the current predefined security policy. - SslPolicy: ELBSecurityPolicy-2015-05 - Certificates: # The ARN of the certificate (only one certficate ARN should be provided) - - CertificateArn: arn:aws:iam::12345678987:server-certificate/test.domain.com - DefaultActions: - - Type: forward # Required. - TargetGroupName: # Required. The name of the target group - state: present - -# Create an ALB with listeners and rules -- elb_application_lb: - name: test-alb - subnets: - - subnet-12345678 - - subnet-87654321 - security_groups: - - sg-12345678 - scheme: internal - listeners: - - Protocol: HTTPS - Port: 443 - DefaultActions: - - Type: forward - TargetGroupName: test-target-group - Certificates: - - CertificateArn: arn:aws:iam::12345678987:server-certificate/test.domain.com - SslPolicy: ELBSecurityPolicy-2015-05 - Rules: - - Conditions: - - Field: path-pattern - Values: - - '/test' - Priority: '1' - Actions: - - TargetGroupName: test-target-group - Type: forward - - Conditions: - - Field: path-pattern - Values: - - "/redirect-path/*" - Priority: '2' - Actions: - - Type: redirect - RedirectConfig: - Host: "#{host}" - Path: "/example/redir" # or /#{path} - Port: "#{port}" - Protocol: "#{protocol}" - Query: "#{query}" - StatusCode: "HTTP_302" # or HTTP_301 - - Conditions: - - Field: path-pattern - Values: - - "/fixed-response-path/" - Priority: '3' - Actions: - - Type: fixed-response - FixedResponseConfig: - ContentType: "text/plain" - MessageBody: "This is the page you're looking for" - StatusCode: "200" - - Conditions: - - Field: host-header - Values: - - "hostname.domain.com" - - "alternate.domain.com" - Priority: '4' - Actions: - - TargetGroupName: test-target-group - Type: forward - state: present - -# Remove an ELB -- elb_application_lb: - name: myelb - state: absent - -''' - -RETURN = ''' -access_logs_s3_bucket: - description: The name of the S3 bucket for the access logs. - returned: when state is present - type: str - sample: mys3bucket -access_logs_s3_enabled: - description: Indicates whether access logs stored in Amazon S3 are enabled. - returned: when state is present - type: str - sample: true -access_logs_s3_prefix: - description: The prefix for the location in the S3 bucket. - returned: when state is present - type: str - sample: my/logs -availability_zones: - description: The Availability Zones for the load balancer. - returned: when state is present - type: list - sample: "[{'subnet_id': 'subnet-aabbccddff', 'zone_name': 'ap-southeast-2a'}]" -canonical_hosted_zone_id: - description: The ID of the Amazon Route 53 hosted zone associated with the load balancer. - returned: when state is present - type: str - sample: ABCDEF12345678 -created_time: - description: The date and time the load balancer was created. - returned: when state is present - type: str - sample: "2015-02-12T02:14:02+00:00" -deletion_protection_enabled: - description: Indicates whether deletion protection is enabled. - returned: when state is present - type: str - sample: true -dns_name: - description: The public DNS name of the load balancer. - returned: when state is present - type: str - sample: internal-my-elb-123456789.ap-southeast-2.elb.amazonaws.com -idle_timeout_timeout_seconds: - description: The idle timeout value, in seconds. - returned: when state is present - type: int - sample: 60 -ip_address_type: - description: The type of IP addresses used by the subnets for the load balancer. - returned: when state is present - type: str - sample: ipv4 -listeners: - description: Information about the listeners. - returned: when state is present - type: complex - contains: - listener_arn: - description: The Amazon Resource Name (ARN) of the listener. - returned: when state is present - type: str - sample: "" - load_balancer_arn: - description: The Amazon Resource Name (ARN) of the load balancer. - returned: when state is present - type: str - sample: "" - port: - description: The port on which the load balancer is listening. - returned: when state is present - type: int - sample: 80 - protocol: - description: The protocol for connections from clients to the load balancer. - returned: when state is present - type: str - sample: HTTPS - certificates: - description: The SSL server certificate. - returned: when state is present - type: complex - contains: - certificate_arn: - description: The Amazon Resource Name (ARN) of the certificate. - returned: when state is present - type: str - sample: "" - ssl_policy: - description: The security policy that defines which ciphers and protocols are supported. - returned: when state is present - type: str - sample: "" - default_actions: - description: The default actions for the listener. - returned: when state is present - type: str - contains: - type: - description: The type of action. - returned: when state is present - type: str - sample: "" - target_group_arn: - description: The Amazon Resource Name (ARN) of the target group. - returned: when state is present - type: str - sample: "" -load_balancer_arn: - description: The Amazon Resource Name (ARN) of the load balancer. - returned: when state is present - type: str - sample: arn:aws:elasticloadbalancing:ap-southeast-2:0123456789:loadbalancer/app/my-elb/001122334455 -load_balancer_name: - description: The name of the load balancer. - returned: when state is present - type: str - sample: my-elb -routing_http2_enabled: - description: Indicates whether HTTP/2 is enabled. - returned: when state is present - type: str - sample: true -scheme: - description: Internet-facing or internal load balancer. - returned: when state is present - type: str - sample: internal -security_groups: - description: The IDs of the security groups for the load balancer. - returned: when state is present - type: list - sample: ['sg-0011223344'] -state: - description: The state of the load balancer. - returned: when state is present - type: dict - sample: "{'code': 'active'}" -tags: - description: The tags attached to the load balancer. - returned: when state is present - type: dict - sample: "{ - 'Tag': 'Example' - }" -type: - description: The type of load balancer. - returned: when state is present - type: str - sample: application -vpc_id: - description: The ID of the VPC for the load balancer. - returned: when state is present - type: str - sample: vpc-0011223344 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict, compare_aws_tags - -from ansible.module_utils.aws.elbv2 import ApplicationLoadBalancer, ELBListeners, ELBListener, ELBListenerRules, ELBListenerRule -from ansible.module_utils.aws.elb_utils import get_elb_listener_rules - - -def create_or_update_elb(elb_obj): - """Create ELB or modify main attributes. json_exit here""" - - if elb_obj.elb: - # ELB exists so check subnets, security groups and tags match what has been passed - - # Subnets - if not elb_obj.compare_subnets(): - elb_obj.modify_subnets() - - # Security Groups - if not elb_obj.compare_security_groups(): - elb_obj.modify_security_groups() - - # Tags - only need to play with tags if tags parameter has been set to something - if elb_obj.tags is not None: - - # Delete necessary tags - tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(elb_obj.elb['tags']), - boto3_tag_list_to_ansible_dict(elb_obj.tags), elb_obj.purge_tags) - if tags_to_delete: - elb_obj.delete_tags(tags_to_delete) - - # Add/update tags - if tags_need_modify: - elb_obj.modify_tags() - - else: - # Create load balancer - elb_obj.create_elb() - - # ELB attributes - elb_obj.update_elb_attributes() - elb_obj.modify_elb_attributes() - - # Listeners - listeners_obj = ELBListeners(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn']) - - listeners_to_add, listeners_to_modify, listeners_to_delete = listeners_obj.compare_listeners() - - # Delete listeners - for listener_to_delete in listeners_to_delete: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_delete, elb_obj.elb['LoadBalancerArn']) - listener_obj.delete() - listeners_obj.changed = True - - # Add listeners - for listener_to_add in listeners_to_add: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_add, elb_obj.elb['LoadBalancerArn']) - listener_obj.add() - listeners_obj.changed = True - - # Modify listeners - for listener_to_modify in listeners_to_modify: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_modify, elb_obj.elb['LoadBalancerArn']) - listener_obj.modify() - listeners_obj.changed = True - - # If listeners changed, mark ELB as changed - if listeners_obj.changed: - elb_obj.changed = True - - # Rules of each listener - for listener in listeners_obj.listeners: - if 'Rules' in listener: - rules_obj = ELBListenerRules(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn'], listener['Rules'], listener['Port']) - - rules_to_add, rules_to_modify, rules_to_delete = rules_obj.compare_rules() - - # Delete rules - if elb_obj.module.params['purge_rules']: - for rule in rules_to_delete: - rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, {'RuleArn': rule}, rules_obj.listener_arn) - rule_obj.delete() - elb_obj.changed = True - - # Add rules - for rule in rules_to_add: - rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn) - rule_obj.create() - elb_obj.changed = True - - # Modify rules - for rule in rules_to_modify: - rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn) - rule_obj.modify() - elb_obj.changed = True - - # Get the ELB again - elb_obj.update() - - # Get the ELB listeners again - listeners_obj.update() - - # Update the ELB attributes - elb_obj.update_elb_attributes() - - # Convert to snake_case and merge in everything we want to return to the user - snaked_elb = camel_dict_to_snake_dict(elb_obj.elb) - snaked_elb.update(camel_dict_to_snake_dict(elb_obj.elb_attributes)) - snaked_elb['listeners'] = [] - for listener in listeners_obj.current_listeners: - # For each listener, get listener rules - listener['rules'] = get_elb_listener_rules(elb_obj.connection, elb_obj.module, listener['ListenerArn']) - snaked_elb['listeners'].append(camel_dict_to_snake_dict(listener)) - - # Change tags to ansible friendly dict - snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(snaked_elb['tags']) - - elb_obj.module.exit_json(changed=elb_obj.changed, **snaked_elb) - - -def delete_elb(elb_obj): - - if elb_obj.elb: - elb_obj.delete() - - elb_obj.module.exit_json(changed=elb_obj.changed) - - -def main(): - - argument_spec = dict( - access_logs_enabled=dict(type='bool'), - access_logs_s3_bucket=dict(type='str'), - access_logs_s3_prefix=dict(type='str'), - deletion_protection=dict(type='bool'), - http2=dict(type='bool'), - idle_timeout=dict(type='int'), - listeners=dict(type='list', - elements='dict', - options=dict( - Protocol=dict(type='str', required=True), - Port=dict(type='int', required=True), - SslPolicy=dict(type='str'), - Certificates=dict(type='list'), - DefaultActions=dict(type='list', required=True), - Rules=dict(type='list') - ) - ), - name=dict(required=True, type='str'), - purge_listeners=dict(default=True, type='bool'), - purge_tags=dict(default=True, type='bool'), - subnets=dict(type='list'), - security_groups=dict(type='list'), - scheme=dict(default='internet-facing', choices=['internet-facing', 'internal']), - state=dict(choices=['present', 'absent'], default='present'), - tags=dict(type='dict'), - wait_timeout=dict(type='int'), - wait=dict(default=False, type='bool'), - purge_rules=dict(default=True, type='bool') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[ - ('state', 'present', ['subnets', 'security_groups']) - ], - required_together=[ - ['access_logs_enabled', 'access_logs_s3_bucket'] - ] - ) - - # Quick check of listeners parameters - listeners = module.params.get("listeners") - if listeners is not None: - for listener in listeners: - for key in listener.keys(): - if key == 'Protocol' and listener[key] == 'HTTPS': - if listener.get('SslPolicy') is None: - module.fail_json(msg="'SslPolicy' is a required listener dict key when Protocol = HTTPS") - - if listener.get('Certificates') is None: - module.fail_json(msg="'Certificates' is a required listener dict key when Protocol = HTTPS") - - connection = module.client('elbv2') - connection_ec2 = module.client('ec2') - - state = module.params.get("state") - - elb = ApplicationLoadBalancer(connection, connection_ec2, module) - - if state == 'present': - create_or_update_elb(elb) - else: - delete_elb(elb) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_application_lb_info.py b/lib/ansible/modules/cloud/amazon/elb_application_lb_info.py deleted file mode 100644 index f2f4c4502a..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_application_lb_info.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: elb_application_lb_info -short_description: Gather information about application ELBs in AWS -description: - - Gather information about application ELBs in AWS - - This module was called C(elb_application_lb_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -author: Rob White (@wimnat) -options: - load_balancer_arns: - description: - - The Amazon Resource Names (ARN) of the load balancers. You can specify up to 20 load balancers in a single call. - required: false - type: list - names: - description: - - The names of the load balancers. - required: false - type: list - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all target groups -- elb_application_lb_info: - -# Gather information about the target group attached to a particular ELB -- elb_application_lb_info: - load_balancer_arns: - - "arn:aws:elasticloadbalancing:ap-southeast-2:001122334455:loadbalancer/app/my-elb/aabbccddeeff" - -# Gather information about a target groups named 'tg1' and 'tg2' -- elb_application_lb_info: - names: - - elb1 - - elb2 - -# Gather information about specific ALB -- elb_application_lb_info: - names: "alb-name" - region: "aws-region" - register: alb_info -- debug: - var: alb_info -''' - -RETURN = ''' -load_balancers: - description: a list of load balancers - returned: always - type: complex - contains: - access_logs_s3_bucket: - description: The name of the S3 bucket for the access logs. - returned: when status is present - type: str - sample: mys3bucket - access_logs_s3_enabled: - description: Indicates whether access logs stored in Amazon S3 are enabled. - returned: when status is present - type: str - sample: true - access_logs_s3_prefix: - description: The prefix for the location in the S3 bucket. - returned: when status is present - type: str - sample: /my/logs - availability_zones: - description: The Availability Zones for the load balancer. - returned: when status is present - type: list - sample: "[{'subnet_id': 'subnet-aabbccddff', 'zone_name': 'ap-southeast-2a'}]" - canonical_hosted_zone_id: - description: The ID of the Amazon Route 53 hosted zone associated with the load balancer. - returned: when status is present - type: str - sample: ABCDEF12345678 - created_time: - description: The date and time the load balancer was created. - returned: when status is present - type: str - sample: "2015-02-12T02:14:02+00:00" - deletion_protection_enabled: - description: Indicates whether deletion protection is enabled. - returned: when status is present - type: str - sample: true - dns_name: - description: The public DNS name of the load balancer. - returned: when status is present - type: str - sample: internal-my-elb-123456789.ap-southeast-2.elb.amazonaws.com - idle_timeout_timeout_seconds: - description: The idle timeout value, in seconds. - returned: when status is present - type: str - sample: 60 - ip_address_type: - description: The type of IP addresses used by the subnets for the load balancer. - returned: when status is present - type: str - sample: ipv4 - load_balancer_arn: - description: The Amazon Resource Name (ARN) of the load balancer. - returned: when status is present - type: str - sample: arn:aws:elasticloadbalancing:ap-southeast-2:0123456789:loadbalancer/app/my-elb/001122334455 - load_balancer_name: - description: The name of the load balancer. - returned: when status is present - type: str - sample: my-elb - scheme: - description: Internet-facing or internal load balancer. - returned: when status is present - type: str - sample: internal - security_groups: - description: The IDs of the security groups for the load balancer. - returned: when status is present - type: list - sample: ['sg-0011223344'] - state: - description: The state of the load balancer. - returned: when status is present - type: dict - sample: "{'code': 'active'}" - tags: - description: The tags attached to the load balancer. - returned: when status is present - type: dict - sample: "{ - 'Tag': 'Example' - }" - type: - description: The type of load balancer. - returned: when status is present - type: str - sample: application - vpc_id: - description: The ID of the VPC for the load balancer. - returned: when status is present - type: str - sample: vpc-0011223344 -''' - -import traceback - -try: - import boto3 - from botocore.exceptions import ClientError, NoCredentialsError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -def get_elb_listeners(connection, module, elb_arn): - - try: - return connection.describe_listeners(LoadBalancerArn=elb_arn)['Listeners'] - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def get_listener_rules(connection, module, listener_arn): - - try: - return connection.describe_rules(ListenerArn=listener_arn)['Rules'] - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def get_load_balancer_attributes(connection, module, load_balancer_arn): - - try: - load_balancer_attributes = boto3_tag_list_to_ansible_dict(connection.describe_load_balancer_attributes(LoadBalancerArn=load_balancer_arn)['Attributes']) - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - # Replace '.' with '_' in attribute key names to make it more Ansibley - for k, v in list(load_balancer_attributes.items()): - load_balancer_attributes[k.replace('.', '_')] = v - del load_balancer_attributes[k] - - return load_balancer_attributes - - -def get_load_balancer_tags(connection, module, load_balancer_arn): - - try: - return boto3_tag_list_to_ansible_dict(connection.describe_tags(ResourceArns=[load_balancer_arn])['TagDescriptions'][0]['Tags']) - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def list_load_balancers(connection, module): - - load_balancer_arns = module.params.get("load_balancer_arns") - names = module.params.get("names") - - try: - load_balancer_paginator = connection.get_paginator('describe_load_balancers') - if not load_balancer_arns and not names: - load_balancers = load_balancer_paginator.paginate().build_full_result() - if load_balancer_arns: - load_balancers = load_balancer_paginator.paginate(LoadBalancerArns=load_balancer_arns).build_full_result() - if names: - load_balancers = load_balancer_paginator.paginate(Names=names).build_full_result() - except ClientError as e: - if e.response['Error']['Code'] == 'LoadBalancerNotFound': - module.exit_json(load_balancers=[]) - else: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except NoCredentialsError as e: - module.fail_json(msg="AWS authentication problem. " + e.message, exception=traceback.format_exc()) - - for load_balancer in load_balancers['LoadBalancers']: - # Get the attributes for each elb - load_balancer.update(get_load_balancer_attributes(connection, module, load_balancer['LoadBalancerArn'])) - - # Get the listeners for each elb - load_balancer['listeners'] = get_elb_listeners(connection, module, load_balancer['LoadBalancerArn']) - - # For each listener, get listener rules - for listener in load_balancer['listeners']: - listener['rules'] = get_listener_rules(connection, module, listener['ListenerArn']) - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_load_balancers = [camel_dict_to_snake_dict(load_balancer) for load_balancer in load_balancers['LoadBalancers']] - - # Get tags for each load balancer - for snaked_load_balancer in snaked_load_balancers: - snaked_load_balancer['tags'] = get_load_balancer_tags(connection, module, snaked_load_balancer['load_balancer_arn']) - - module.exit_json(load_balancers=snaked_load_balancers) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - load_balancer_arns=dict(type='list'), - names=dict(type='list') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[['load_balancer_arns', 'names']], - supports_check_mode=True - ) - if module._name == 'elb_application_lb_facts': - module.deprecate("The 'elb_application_lb_facts' module has been renamed to 'elb_application_lb_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_load_balancers(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_classic_lb.py b/lib/ansible/modules/cloud/amazon/elb_classic_lb.py deleted file mode 100644 index e51b376b82..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_classic_lb.py +++ /dev/null @@ -1,1365 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: elb_classic_lb -description: - - Returns information about the load balancer. - - Will be marked changed when called only if state is changed. -short_description: Creates or destroys Amazon ELB. -version_added: "2.4" -author: - - "Jim Dalton (@jsdalton)" -options: - state: - description: - - Create or destroy the ELB - choices: ["present", "absent"] - required: true - type: str - name: - description: - - The name of the ELB - required: true - type: str - listeners: - description: - - List of ports/protocols for this ELB to listen on (see example) - type: list - purge_listeners: - description: - - Purge existing listeners on ELB that are not found in listeners - type: bool - default: 'yes' - instance_ids: - description: - - List of instance ids to attach to this ELB - version_added: "2.1" - type: list - purge_instance_ids: - description: - - Purge existing instance ids on ELB that are not found in instance_ids - type: bool - default: 'no' - version_added: "2.1" - zones: - description: - - List of availability zones to enable on this ELB - type: list - purge_zones: - description: - - Purge existing availability zones on ELB that are not found in zones - type: bool - default: 'no' - security_group_ids: - description: - - A list of security groups to apply to the elb - version_added: "1.6" - type: list - security_group_names: - description: - - A list of security group names to apply to the elb - version_added: "2.0" - type: list - health_check: - description: - - An associative array of health check configuration settings (see example) - type: dict - access_logs: - description: - - An associative array of access logs configuration settings (see example) - version_added: "2.0" - type: dict - subnets: - description: - - A list of VPC subnets to use when creating ELB. Zones should be empty if using this. - version_added: "1.7" - type: list - purge_subnets: - description: - - Purge existing subnet on ELB that are not found in subnets - type: bool - default: 'no' - version_added: "1.7" - scheme: - description: - - The scheme to use when creating the ELB. For a private VPC-visible ELB use 'internal'. - If you choose to update your scheme with a different value the ELB will be destroyed and - recreated. To update scheme you must use the option wait. - choices: ["internal", "internet-facing"] - default: 'internet-facing' - version_added: "1.7" - type: str - validate_certs: - description: - - When set to C(no), SSL certificates will not be validated for boto versions >= 2.6.0. - type: bool - default: 'yes' - version_added: "1.5" - connection_draining_timeout: - description: - - Wait a specified timeout allowing connections to drain before terminating an instance - version_added: "1.8" - type: int - idle_timeout: - description: - - ELB connections from clients and to servers are timed out after this amount of time - version_added: "2.0" - type: int - cross_az_load_balancing: - description: - - Distribute load across all configured Availability Zones - type: bool - default: 'no' - version_added: "1.8" - stickiness: - description: - - An associative array of stickiness policy settings. Policy will be applied to all listeners ( see example ) - version_added: "2.0" - type: dict - wait: - description: - - When specified, Ansible will check the status of the load balancer to ensure it has been successfully - removed from AWS. - type: bool - default: 'no' - version_added: "2.1" - wait_timeout: - description: - - Used in conjunction with wait. Number of seconds to wait for the elb to be terminated. - A maximum of 600 seconds (10 minutes) is allowed. - default: 60 - version_added: "2.1" - type: int - tags: - description: - - An associative array of tags. To delete all tags, supply an empty dict. - version_added: "2.1" - type: dict - -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. - -# Basic provisioning example (non-VPC) - -- elb_classic_lb: - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http # options are http, https, ssl, tcp - load_balancer_port: 80 - instance_port: 80 - proxy_protocol: True - - protocol: https - load_balancer_port: 443 - instance_protocol: http # optional, defaults to value of protocol setting - instance_port: 80 - # ssl certificate required for https or ssl - ssl_certificate_id: "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert" - delegate_to: localhost - -# Internal ELB example - -- elb_classic_lb: - name: "test-vpc" - scheme: internal - state: present - instance_ids: - - i-abcd1234 - purge_instance_ids: true - subnets: - - subnet-abcd1234 - - subnet-1a2b3c4d - listeners: - - protocol: http # options are http, https, ssl, tcp - load_balancer_port: 80 - instance_port: 80 - delegate_to: localhost - -# Configure a health check and the access logs -- elb_classic_lb: - name: "test-please-delete" - state: present - zones: - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - health_check: - ping_protocol: http # options are http, https, ssl, tcp - ping_port: 80 - ping_path: "/index.html" # not required for tcp or ssl - response_timeout: 5 # seconds - interval: 30 # seconds - unhealthy_threshold: 2 - healthy_threshold: 10 - access_logs: - interval: 5 # minutes (defaults to 60) - s3_location: "my-bucket" # This value is required if access_logs is set - s3_prefix: "logs" - delegate_to: localhost - -# Ensure ELB is gone -- elb_classic_lb: - name: "test-please-delete" - state: absent - delegate_to: localhost - -# Ensure ELB is gone and wait for check (for default timeout) -- elb_classic_lb: - name: "test-please-delete" - state: absent - wait: yes - delegate_to: localhost - -# Ensure ELB is gone and wait for check with timeout value -- elb_classic_lb: - name: "test-please-delete" - state: absent - wait: yes - wait_timeout: 600 - delegate_to: localhost - -# Normally, this module will purge any listeners that exist on the ELB -# but aren't specified in the listeners parameter. If purge_listeners is -# false it leaves them alone -- elb_classic_lb: - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_listeners: no - delegate_to: localhost - -# Normally, this module will leave availability zones that are enabled -# on the ELB alone. If purge_zones is true, then any extraneous zones -# will be removed -- elb_classic_lb: - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - delegate_to: localhost - -# Creates a ELB and assigns a list of subnets to it. -- elb_classic_lb: - state: present - name: 'New ELB' - security_group_ids: 'sg-123456, sg-67890' - region: us-west-2 - subnets: 'subnet-123456,subnet-67890' - purge_subnets: yes - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - delegate_to: localhost - -# Create an ELB with connection draining, increased idle timeout and cross availability -# zone load balancing -- elb_classic_lb: - name: "New ELB" - state: present - connection_draining_timeout: 60 - idle_timeout: 300 - cross_az_load_balancing: "yes" - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - delegate_to: localhost - -# Create an ELB with load balancer stickiness enabled -- elb_classic_lb: - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - stickiness: - type: loadbalancer - enabled: yes - expiration: 300 - delegate_to: localhost - -# Create an ELB with application stickiness enabled -- elb_classic_lb: - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - stickiness: - type: application - enabled: yes - cookie: SESSIONID - delegate_to: localhost - -# Create an ELB and add tags -- elb_classic_lb: - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - tags: - Name: "New ELB" - stack: "production" - client: "Bob" - delegate_to: localhost - -# Delete all tags from an ELB -- elb_classic_lb: - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - tags: {} - delegate_to: localhost -""" - -import random -import time -import traceback - -try: - import boto - import boto.ec2.elb - import boto.ec2.elb.attributes - import boto.vpc - from boto.ec2.elb.healthcheck import HealthCheck - from boto.ec2.tag import Tag - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, connect_to_aws, AnsibleAWSError, get_aws_connection_info -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native - - -def _throttleable_operation(max_retries): - def _operation_wrapper(op): - def _do_op(*args, **kwargs): - retry = 0 - while True: - try: - return op(*args, **kwargs) - except boto.exception.BotoServerError as e: - if retry < max_retries and e.code in \ - ("Throttling", "RequestLimitExceeded"): - retry = retry + 1 - time.sleep(min(random.random() * (2 ** retry), 300)) - continue - else: - raise - return _do_op - return _operation_wrapper - - -def _get_vpc_connection(module, region, aws_connect_params): - try: - return connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - - -_THROTTLING_RETRIES = 5 - - -class ElbManager(object): - """Handles ELB creation and destruction""" - - def __init__(self, module, name, listeners=None, purge_listeners=None, - zones=None, purge_zones=None, security_group_ids=None, - health_check=None, subnets=None, purge_subnets=None, - scheme="internet-facing", connection_draining_timeout=None, - idle_timeout=None, - cross_az_load_balancing=None, access_logs=None, - stickiness=None, wait=None, wait_timeout=None, tags=None, - region=None, - instance_ids=None, purge_instance_ids=None, **aws_connect_params): - - self.module = module - self.name = name - self.listeners = listeners - self.purge_listeners = purge_listeners - self.instance_ids = instance_ids - self.purge_instance_ids = purge_instance_ids - self.zones = zones - self.purge_zones = purge_zones - self.security_group_ids = security_group_ids - self.health_check = health_check - self.subnets = subnets - self.purge_subnets = purge_subnets - self.scheme = scheme - self.connection_draining_timeout = connection_draining_timeout - self.idle_timeout = idle_timeout - self.cross_az_load_balancing = cross_az_load_balancing - self.access_logs = access_logs - self.stickiness = stickiness - self.wait = wait - self.wait_timeout = wait_timeout - self.tags = tags - - self.aws_connect_params = aws_connect_params - self.region = region - - self.changed = False - self.status = 'gone' - self.elb_conn = self._get_elb_connection() - - try: - self.elb = self._get_elb() - except boto.exception.BotoServerError as e: - module.fail_json(msg='unable to get all load balancers: %s' % e.message, exception=traceback.format_exc()) - - self.ec2_conn = self._get_ec2_connection() - - @_throttleable_operation(_THROTTLING_RETRIES) - def ensure_ok(self): - """Create the ELB""" - if not self.elb: - # Zones and listeners will be added at creation - self._create_elb() - else: - if self._get_scheme(): - # the only way to change the scheme is by recreating the resource - self.ensure_gone() - self._create_elb() - else: - self._set_zones() - self._set_security_groups() - self._set_elb_listeners() - self._set_subnets() - self._set_health_check() - # boto has introduced support for some ELB attributes in - # different versions, so we check first before trying to - # set them to avoid errors - if self._check_attribute_support('connection_draining'): - self._set_connection_draining_timeout() - if self._check_attribute_support('connecting_settings'): - self._set_idle_timeout() - if self._check_attribute_support('cross_zone_load_balancing'): - self._set_cross_az_load_balancing() - if self._check_attribute_support('access_log'): - self._set_access_log() - # add sticky options - self.select_stickiness_policy() - - # ensure backend server policies are correct - self._set_backend_policies() - # set/remove instance ids - self._set_instance_ids() - - self._set_tags() - - def ensure_gone(self): - """Destroy the ELB""" - if self.elb: - self._delete_elb() - if self.wait: - elb_removed = self._wait_for_elb_removed() - # Unfortunately even though the ELB itself is removed quickly - # the interfaces take longer so reliant security groups cannot - # be deleted until the interface has registered as removed. - elb_interface_removed = self._wait_for_elb_interface_removed() - if not (elb_removed and elb_interface_removed): - self.module.fail_json(msg='Timed out waiting for removal of load balancer.') - - def get_info(self): - try: - check_elb = self.elb_conn.get_all_load_balancers(self.name)[0] - except Exception: - check_elb = None - - if not check_elb: - info = { - 'name': self.name, - 'status': self.status, - 'region': self.region - } - else: - try: - lb_cookie_policy = check_elb.policies.lb_cookie_stickiness_policies[0].__dict__['policy_name'] - except Exception: - lb_cookie_policy = None - try: - app_cookie_policy = check_elb.policies.app_cookie_stickiness_policies[0].__dict__['policy_name'] - except Exception: - app_cookie_policy = None - - info = { - 'name': check_elb.name, - 'dns_name': check_elb.dns_name, - 'zones': check_elb.availability_zones, - 'security_group_ids': check_elb.security_groups, - 'status': self.status, - 'subnets': self.subnets, - 'scheme': check_elb.scheme, - 'hosted_zone_name': check_elb.canonical_hosted_zone_name, - 'hosted_zone_id': check_elb.canonical_hosted_zone_name_id, - 'lb_cookie_policy': lb_cookie_policy, - 'app_cookie_policy': app_cookie_policy, - 'proxy_policy': self._get_proxy_protocol_policy(), - 'backends': self._get_backend_policies(), - 'instances': [instance.id for instance in check_elb.instances], - 'out_of_service_count': 0, - 'in_service_count': 0, - 'unknown_instance_state_count': 0, - 'region': self.region - } - - # status of instances behind the ELB - if info['instances']: - info['instance_health'] = [dict( - instance_id=instance_state.instance_id, - reason_code=instance_state.reason_code, - state=instance_state.state - ) for instance_state in self.elb_conn.describe_instance_health(self.name)] - else: - info['instance_health'] = [] - - # instance state counts: InService or OutOfService - if info['instance_health']: - for instance_state in info['instance_health']: - if instance_state['state'] == "InService": - info['in_service_count'] += 1 - elif instance_state['state'] == "OutOfService": - info['out_of_service_count'] += 1 - else: - info['unknown_instance_state_count'] += 1 - - if check_elb.health_check: - info['health_check'] = { - 'target': check_elb.health_check.target, - 'interval': check_elb.health_check.interval, - 'timeout': check_elb.health_check.timeout, - 'healthy_threshold': check_elb.health_check.healthy_threshold, - 'unhealthy_threshold': check_elb.health_check.unhealthy_threshold, - } - - if check_elb.listeners: - info['listeners'] = [self._api_listener_as_tuple(l) - for l in check_elb.listeners] - elif self.status == 'created': - # When creating a new ELB, listeners don't show in the - # immediately returned result, so just include the - # ones that were added - info['listeners'] = [self._listener_as_tuple(l) - for l in self.listeners] - else: - info['listeners'] = [] - - if self._check_attribute_support('connection_draining'): - info['connection_draining_timeout'] = int(self.elb_conn.get_lb_attribute(self.name, 'ConnectionDraining').timeout) - - if self._check_attribute_support('connecting_settings'): - info['idle_timeout'] = self.elb_conn.get_lb_attribute(self.name, 'ConnectingSettings').idle_timeout - - if self._check_attribute_support('cross_zone_load_balancing'): - is_cross_az_lb_enabled = self.elb_conn.get_lb_attribute(self.name, 'CrossZoneLoadBalancing') - if is_cross_az_lb_enabled: - info['cross_az_load_balancing'] = 'yes' - else: - info['cross_az_load_balancing'] = 'no' - - # return stickiness info? - - info['tags'] = self.tags - - return info - - @_throttleable_operation(_THROTTLING_RETRIES) - def _wait_for_elb_removed(self): - polling_increment_secs = 15 - max_retries = (self.wait_timeout // polling_increment_secs) - status_achieved = False - - for x in range(0, max_retries): - try: - self.elb_conn.get_all_lb_attributes(self.name) - except (boto.exception.BotoServerError, Exception) as e: - if "LoadBalancerNotFound" in e.code: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - - return status_achieved - - @_throttleable_operation(_THROTTLING_RETRIES) - def _wait_for_elb_interface_removed(self): - polling_increment_secs = 15 - max_retries = (self.wait_timeout // polling_increment_secs) - status_achieved = False - - elb_interfaces = self.ec2_conn.get_all_network_interfaces( - filters={'attachment.instance-owner-id': 'amazon-elb', - 'description': 'ELB {0}'.format(self.name)}) - - for x in range(0, max_retries): - for interface in elb_interfaces: - try: - result = self.ec2_conn.get_all_network_interfaces(interface.id) - if result == []: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - except (boto.exception.BotoServerError, Exception) as e: - if 'InvalidNetworkInterfaceID' in e.code: - status_achieved = True - break - else: - self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - return status_achieved - - @_throttleable_operation(_THROTTLING_RETRIES) - def _get_elb(self): - elbs = self.elb_conn.get_all_load_balancers() - for elb in elbs: - if self.name == elb.name: - self.status = 'ok' - return elb - - def _get_elb_connection(self): - try: - return connect_to_aws(boto.ec2.elb, self.region, - **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - def _get_ec2_connection(self): - try: - return connect_to_aws(boto.ec2, self.region, - **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, Exception) as e: - self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - @_throttleable_operation(_THROTTLING_RETRIES) - def _delete_elb(self): - # True if succeeds, exception raised if not - result = self.elb_conn.delete_load_balancer(name=self.name) - if result: - self.changed = True - self.status = 'deleted' - - def _create_elb(self): - listeners = [self._listener_as_tuple(l) for l in self.listeners] - self.elb = self.elb_conn.create_load_balancer(name=self.name, - zones=self.zones, - security_groups=self.security_group_ids, - complex_listeners=listeners, - subnets=self.subnets, - scheme=self.scheme) - if self.elb: - # HACK: Work around a boto bug in which the listeners attribute is - # always set to the listeners argument to create_load_balancer, and - # not the complex_listeners - # We're not doing a self.elb = self._get_elb here because there - # might be eventual consistency issues and it doesn't necessarily - # make sense to wait until the ELB gets returned from the EC2 API. - # This is necessary in the event we hit the throttling errors and - # need to retry ensure_ok - # See https://github.com/boto/boto/issues/3526 - self.elb.listeners = self.listeners - self.changed = True - self.status = 'created' - - def _create_elb_listeners(self, listeners): - """Takes a list of listener tuples and creates them""" - # True if succeeds, exception raised if not - self.changed = self.elb_conn.create_load_balancer_listeners(self.name, - complex_listeners=listeners) - - def _delete_elb_listeners(self, listeners): - """Takes a list of listener tuples and deletes them from the elb""" - ports = [l[0] for l in listeners] - - # True if succeeds, exception raised if not - self.changed = self.elb_conn.delete_load_balancer_listeners(self.name, - ports) - - def _set_elb_listeners(self): - """ - Creates listeners specified by self.listeners; overwrites existing - listeners on these ports; removes extraneous listeners - """ - listeners_to_add = [] - listeners_to_remove = [] - listeners_to_keep = [] - - # Check for any listeners we need to create or overwrite - for listener in self.listeners: - listener_as_tuple = self._listener_as_tuple(listener) - - # First we loop through existing listeners to see if one is - # already specified for this port - existing_listener_found = None - for existing_listener in self.elb.listeners: - # Since ELB allows only one listener on each incoming port, a - # single match on the incoming port is all we're looking for - if existing_listener[0] == int(listener['load_balancer_port']): - existing_listener_found = self._api_listener_as_tuple(existing_listener) - break - - if existing_listener_found: - # Does it match exactly? - if listener_as_tuple != existing_listener_found: - # The ports are the same but something else is different, - # so we'll remove the existing one and add the new one - listeners_to_remove.append(existing_listener_found) - listeners_to_add.append(listener_as_tuple) - else: - # We already have this listener, so we're going to keep it - listeners_to_keep.append(existing_listener_found) - else: - # We didn't find an existing listener, so just add the new one - listeners_to_add.append(listener_as_tuple) - - # Check for any extraneous listeners we need to remove, if desired - if self.purge_listeners: - for existing_listener in self.elb.listeners: - existing_listener_tuple = self._api_listener_as_tuple(existing_listener) - if existing_listener_tuple in listeners_to_remove: - # Already queued for removal - continue - if existing_listener_tuple in listeners_to_keep: - # Keep this one around - continue - # Since we're not already removing it and we don't need to keep - # it, let's get rid of it - listeners_to_remove.append(existing_listener_tuple) - - if listeners_to_remove: - self._delete_elb_listeners(listeners_to_remove) - - if listeners_to_add: - self._create_elb_listeners(listeners_to_add) - - def _api_listener_as_tuple(self, listener): - """Adds ssl_certificate_id to ELB API tuple if present""" - base_tuple = listener.get_complex_tuple() - if listener.ssl_certificate_id and len(base_tuple) < 5: - return base_tuple + (listener.ssl_certificate_id,) - return base_tuple - - def _listener_as_tuple(self, listener): - """Formats listener as a 4- or 5-tuples, in the order specified by the - ELB API""" - # N.B. string manipulations on protocols below (str(), upper()) is to - # ensure format matches output from ELB API - listener_list = [ - int(listener['load_balancer_port']), - int(listener['instance_port']), - str(listener['protocol'].upper()), - ] - - # Instance protocol is not required by ELB API; it defaults to match - # load balancer protocol. We'll mimic that behavior here - if 'instance_protocol' in listener: - listener_list.append(str(listener['instance_protocol'].upper())) - else: - listener_list.append(str(listener['protocol'].upper())) - - if 'ssl_certificate_id' in listener: - listener_list.append(str(listener['ssl_certificate_id'])) - - return tuple(listener_list) - - def _enable_zones(self, zones): - try: - self.elb.enable_zones(zones) - except boto.exception.BotoServerError as e: - self.module.fail_json(msg='unable to enable zones: %s' % e.message, exception=traceback.format_exc()) - - self.changed = True - - def _disable_zones(self, zones): - try: - self.elb.disable_zones(zones) - except boto.exception.BotoServerError as e: - self.module.fail_json(msg='unable to disable zones: %s' % e.message, exception=traceback.format_exc()) - self.changed = True - - def _attach_subnets(self, subnets): - self.elb_conn.attach_lb_to_subnets(self.name, subnets) - self.changed = True - - def _detach_subnets(self, subnets): - self.elb_conn.detach_lb_from_subnets(self.name, subnets) - self.changed = True - - def _set_subnets(self): - """Determine which subnets need to be attached or detached on the ELB""" - if self.subnets: - if self.purge_subnets: - subnets_to_detach = list(set(self.elb.subnets) - set(self.subnets)) - subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) - else: - subnets_to_detach = None - subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) - - if subnets_to_attach: - self._attach_subnets(subnets_to_attach) - if subnets_to_detach: - self._detach_subnets(subnets_to_detach) - - def _get_scheme(self): - """Determine if the current scheme is different than the scheme of the ELB""" - if self.scheme: - if self.elb.scheme != self.scheme: - if not self.wait: - self.module.fail_json(msg="Unable to modify scheme without using the wait option") - return True - return False - - def _set_zones(self): - """Determine which zones need to be enabled or disabled on the ELB""" - if self.zones: - if self.purge_zones: - zones_to_disable = list(set(self.elb.availability_zones) - - set(self.zones)) - zones_to_enable = list(set(self.zones) - - set(self.elb.availability_zones)) - else: - zones_to_disable = None - zones_to_enable = list(set(self.zones) - - set(self.elb.availability_zones)) - if zones_to_enable: - self._enable_zones(zones_to_enable) - # N.B. This must come second, in case it would have removed all zones - if zones_to_disable: - self._disable_zones(zones_to_disable) - - def _set_security_groups(self): - if self.security_group_ids is not None and set(self.elb.security_groups) != set(self.security_group_ids): - self.elb_conn.apply_security_groups_to_lb(self.name, self.security_group_ids) - self.changed = True - - def _set_health_check(self): - """Set health check values on ELB as needed""" - if self.health_check: - # This just makes it easier to compare each of the attributes - # and look for changes. Keys are attributes of the current - # health_check; values are desired values of new health_check - health_check_config = { - "target": self._get_health_check_target(), - "timeout": self.health_check['response_timeout'], - "interval": self.health_check['interval'], - "unhealthy_threshold": self.health_check['unhealthy_threshold'], - "healthy_threshold": self.health_check['healthy_threshold'], - } - - update_health_check = False - - # The health_check attribute is *not* set on newly created - # ELBs! So we have to create our own. - if not self.elb.health_check: - self.elb.health_check = HealthCheck() - - for attr, desired_value in health_check_config.items(): - if getattr(self.elb.health_check, attr) != desired_value: - setattr(self.elb.health_check, attr, desired_value) - update_health_check = True - - if update_health_check: - self.elb.configure_health_check(self.elb.health_check) - self.changed = True - - def _check_attribute_support(self, attr): - return hasattr(boto.ec2.elb.attributes.LbAttributes(), attr) - - def _set_cross_az_load_balancing(self): - attributes = self.elb.get_attributes() - if self.cross_az_load_balancing: - if not attributes.cross_zone_load_balancing.enabled: - self.changed = True - attributes.cross_zone_load_balancing.enabled = True - else: - if attributes.cross_zone_load_balancing.enabled: - self.changed = True - attributes.cross_zone_load_balancing.enabled = False - self.elb_conn.modify_lb_attribute(self.name, 'CrossZoneLoadBalancing', - attributes.cross_zone_load_balancing.enabled) - - def _set_access_log(self): - attributes = self.elb.get_attributes() - if self.access_logs: - if 's3_location' not in self.access_logs: - self.module.fail_json(msg='s3_location information required') - - access_logs_config = { - "enabled": True, - "s3_bucket_name": self.access_logs['s3_location'], - "s3_bucket_prefix": self.access_logs.get('s3_prefix', ''), - "emit_interval": self.access_logs.get('interval', 60), - } - - update_access_logs_config = False - for attr, desired_value in access_logs_config.items(): - if getattr(attributes.access_log, attr) != desired_value: - setattr(attributes.access_log, attr, desired_value) - update_access_logs_config = True - if update_access_logs_config: - self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) - self.changed = True - elif attributes.access_log.enabled: - attributes.access_log.enabled = False - self.changed = True - self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) - - def _set_connection_draining_timeout(self): - attributes = self.elb.get_attributes() - if self.connection_draining_timeout is not None: - if not attributes.connection_draining.enabled or \ - attributes.connection_draining.timeout != self.connection_draining_timeout: - self.changed = True - attributes.connection_draining.enabled = True - attributes.connection_draining.timeout = self.connection_draining_timeout - self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) - else: - if attributes.connection_draining.enabled: - self.changed = True - attributes.connection_draining.enabled = False - self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) - - def _set_idle_timeout(self): - attributes = self.elb.get_attributes() - if self.idle_timeout is not None: - if attributes.connecting_settings.idle_timeout != self.idle_timeout: - self.changed = True - attributes.connecting_settings.idle_timeout = self.idle_timeout - self.elb_conn.modify_lb_attribute(self.name, 'ConnectingSettings', attributes.connecting_settings) - - def _policy_name(self, policy_type): - return 'elb-classic-lb-{0}'.format(to_native(policy_type, errors='surrogate_or_strict')) - - def _create_policy(self, policy_param, policy_meth, policy): - getattr(self.elb_conn, policy_meth)(policy_param, self.elb.name, policy) - - def _delete_policy(self, elb_name, policy): - self.elb_conn.delete_lb_policy(elb_name, policy) - - def _update_policy(self, policy_param, policy_meth, policy_attr, policy): - self._delete_policy(self.elb.name, policy) - self._create_policy(policy_param, policy_meth, policy) - - def _set_listener_policy(self, listeners_dict, policy=None): - policy = [] if policy is None else policy - - for listener_port in listeners_dict: - if listeners_dict[listener_port].startswith('HTTP'): - self.elb_conn.set_lb_policies_of_listener(self.elb.name, listener_port, policy) - - def _set_stickiness_policy(self, elb_info, listeners_dict, policy, **policy_attrs): - for p in getattr(elb_info.policies, policy_attrs['attr']): - if str(p.__dict__['policy_name']) == str(policy[0]): - if str(p.__dict__[policy_attrs['dict_key']]) != str(policy_attrs['param_value'] or 0): - self._set_listener_policy(listeners_dict) - self._update_policy(policy_attrs['param_value'], policy_attrs['method'], policy_attrs['attr'], policy[0]) - self.changed = True - break - else: - self._create_policy(policy_attrs['param_value'], policy_attrs['method'], policy[0]) - self.changed = True - - self._set_listener_policy(listeners_dict, policy) - - def select_stickiness_policy(self): - if self.stickiness: - - if 'cookie' in self.stickiness and 'expiration' in self.stickiness: - self.module.fail_json(msg='\'cookie\' and \'expiration\' can not be set at the same time') - - elb_info = self.elb_conn.get_all_load_balancers(self.elb.name)[0] - d = {} - for listener in elb_info.listeners: - d[listener[0]] = listener[2] - listeners_dict = d - - if self.stickiness['type'] == 'loadbalancer': - policy = [] - policy_type = 'LBCookieStickinessPolicyType' - - if self.module.boolean(self.stickiness['enabled']): - - if 'expiration' not in self.stickiness: - self.module.fail_json(msg='expiration must be set when type is loadbalancer') - - try: - expiration = self.stickiness['expiration'] if int(self.stickiness['expiration']) else None - except ValueError: - self.module.fail_json(msg='expiration must be set to an integer') - - policy_attrs = { - 'type': policy_type, - 'attr': 'lb_cookie_stickiness_policies', - 'method': 'create_lb_cookie_stickiness_policy', - 'dict_key': 'cookie_expiration_period', - 'param_value': expiration - } - policy.append(self._policy_name(policy_attrs['type'])) - - self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif not self.module.boolean(self.stickiness['enabled']): - if len(elb_info.policies.lb_cookie_stickiness_policies): - if elb_info.policies.lb_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): - self.changed = True - else: - self.changed = False - self._set_listener_policy(listeners_dict) - self._delete_policy(self.elb.name, self._policy_name(policy_type)) - - elif self.stickiness['type'] == 'application': - policy = [] - policy_type = 'AppCookieStickinessPolicyType' - if self.module.boolean(self.stickiness['enabled']): - - if 'cookie' not in self.stickiness: - self.module.fail_json(msg='cookie must be set when type is application') - - policy_attrs = { - 'type': policy_type, - 'attr': 'app_cookie_stickiness_policies', - 'method': 'create_app_cookie_stickiness_policy', - 'dict_key': 'cookie_name', - 'param_value': self.stickiness['cookie'] - } - policy.append(self._policy_name(policy_attrs['type'])) - self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif not self.module.boolean(self.stickiness['enabled']): - if len(elb_info.policies.app_cookie_stickiness_policies): - if elb_info.policies.app_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): - self.changed = True - self._set_listener_policy(listeners_dict) - self._delete_policy(self.elb.name, self._policy_name(policy_type)) - - else: - self._set_listener_policy(listeners_dict) - - def _get_backend_policies(self): - """Get a list of backend policies""" - policies = [] - if self.elb.backends is not None: - for backend in self.elb.backends: - if backend.policies is not None: - for policy in backend.policies: - policies.append(str(backend.instance_port) + ':' + policy.policy_name) - - return policies - - def _set_backend_policies(self): - """Sets policies for all backends""" - ensure_proxy_protocol = False - replace = [] - backend_policies = self._get_backend_policies() - - # Find out what needs to be changed - for listener in self.listeners: - want = False - - if 'proxy_protocol' in listener and listener['proxy_protocol']: - ensure_proxy_protocol = True - want = True - - if str(listener['instance_port']) + ':ProxyProtocol-policy' in backend_policies: - if not want: - replace.append({'port': listener['instance_port'], 'policies': []}) - elif want: - replace.append({'port': listener['instance_port'], 'policies': ['ProxyProtocol-policy']}) - - # enable or disable proxy protocol - if ensure_proxy_protocol: - self._set_proxy_protocol_policy() - - # Make the backend policies so - for item in replace: - self.elb_conn.set_lb_policies_of_backend_server(self.elb.name, item['port'], item['policies']) - self.changed = True - - def _get_proxy_protocol_policy(self): - """Find out if the elb has a proxy protocol enabled""" - if self.elb.policies is not None and self.elb.policies.other_policies is not None: - for policy in self.elb.policies.other_policies: - if policy.policy_name == 'ProxyProtocol-policy': - return policy.policy_name - - return None - - def _set_proxy_protocol_policy(self): - """Install a proxy protocol policy if needed""" - proxy_policy = self._get_proxy_protocol_policy() - - if proxy_policy is None: - self.elb_conn.create_lb_policy( - self.elb.name, 'ProxyProtocol-policy', 'ProxyProtocolPolicyType', {'ProxyProtocol': True} - ) - self.changed = True - - # TODO: remove proxy protocol policy if not needed anymore? There is no side effect to leaving it there - - def _diff_list(self, a, b): - """Find the entries in list a that are not in list b""" - b = set(b) - return [aa for aa in a if aa not in b] - - def _get_instance_ids(self): - """Get the current list of instance ids installed in the elb""" - instances = [] - if self.elb.instances is not None: - for instance in self.elb.instances: - instances.append(instance.id) - - return instances - - def _set_instance_ids(self): - """Register or deregister instances from an lb instance""" - assert_instances = self.instance_ids or [] - - has_instances = self._get_instance_ids() - - add_instances = self._diff_list(assert_instances, has_instances) - if add_instances: - self.elb_conn.register_instances(self.elb.name, add_instances) - self.changed = True - - if self.purge_instance_ids: - remove_instances = self._diff_list(has_instances, assert_instances) - if remove_instances: - self.elb_conn.deregister_instances(self.elb.name, remove_instances) - self.changed = True - - def _set_tags(self): - """Add/Delete tags""" - if self.tags is None: - return - - params = {'LoadBalancerNames.member.1': self.name} - - tagdict = dict() - - # get the current list of tags from the ELB, if ELB exists - if self.elb: - current_tags = self.elb_conn.get_list('DescribeTags', params, - [('member', Tag)]) - tagdict = dict((tag.Key, tag.Value) for tag in current_tags - if hasattr(tag, 'Key')) - - # Add missing tags - dictact = dict(set(self.tags.items()) - set(tagdict.items())) - if dictact: - for i, key in enumerate(dictact): - params['Tags.member.%d.Key' % (i + 1)] = key - params['Tags.member.%d.Value' % (i + 1)] = dictact[key] - - self.elb_conn.make_request('AddTags', params) - self.changed = True - - # Remove extra tags - dictact = dict(set(tagdict.items()) - set(self.tags.items())) - if dictact: - for i, key in enumerate(dictact): - params['Tags.member.%d.Key' % (i + 1)] = key - - self.elb_conn.make_request('RemoveTags', params) - self.changed = True - - def _get_health_check_target(self): - """Compose target string from healthcheck parameters""" - protocol = self.health_check['ping_protocol'].upper() - path = "" - - if protocol in ['HTTP', 'HTTPS'] and 'ping_path' in self.health_check: - path = self.health_check['ping_path'] - - return "%s:%s%s" % (protocol, self.health_check['ping_port'], path) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state={'required': True, 'choices': ['present', 'absent']}, - name={'required': True}, - listeners={'default': None, 'required': False, 'type': 'list'}, - purge_listeners={'default': True, 'required': False, 'type': 'bool'}, - instance_ids={'default': None, 'required': False, 'type': 'list'}, - purge_instance_ids={'default': False, 'required': False, 'type': 'bool'}, - zones={'default': None, 'required': False, 'type': 'list'}, - purge_zones={'default': False, 'required': False, 'type': 'bool'}, - security_group_ids={'default': None, 'required': False, 'type': 'list'}, - security_group_names={'default': None, 'required': False, 'type': 'list'}, - health_check={'default': None, 'required': False, 'type': 'dict'}, - subnets={'default': None, 'required': False, 'type': 'list'}, - purge_subnets={'default': False, 'required': False, 'type': 'bool'}, - scheme={'default': 'internet-facing', 'required': False, 'choices': ['internal', 'internet-facing']}, - connection_draining_timeout={'default': None, 'required': False, 'type': 'int'}, - idle_timeout={'default': None, 'type': 'int', 'required': False}, - cross_az_load_balancing={'default': None, 'type': 'bool', 'required': False}, - stickiness={'default': None, 'required': False, 'type': 'dict'}, - access_logs={'default': None, 'required': False, 'type': 'dict'}, - wait={'default': False, 'type': 'bool', 'required': False}, - wait_timeout={'default': 60, 'type': 'int', 'required': False}, - tags={'default': None, 'required': False, 'type': 'dict'} - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['security_group_ids', 'security_group_names']] - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - - name = module.params['name'] - state = module.params['state'] - listeners = module.params['listeners'] - purge_listeners = module.params['purge_listeners'] - instance_ids = module.params['instance_ids'] - purge_instance_ids = module.params['purge_instance_ids'] - zones = module.params['zones'] - purge_zones = module.params['purge_zones'] - security_group_ids = module.params['security_group_ids'] - security_group_names = module.params['security_group_names'] - health_check = module.params['health_check'] - access_logs = module.params['access_logs'] - subnets = module.params['subnets'] - purge_subnets = module.params['purge_subnets'] - scheme = module.params['scheme'] - connection_draining_timeout = module.params['connection_draining_timeout'] - idle_timeout = module.params['idle_timeout'] - cross_az_load_balancing = module.params['cross_az_load_balancing'] - stickiness = module.params['stickiness'] - wait = module.params['wait'] - wait_timeout = module.params['wait_timeout'] - tags = module.params['tags'] - - if state == 'present' and not listeners: - module.fail_json(msg="At least one listener is required for ELB creation") - - if state == 'present' and not (zones or subnets): - module.fail_json(msg="At least one availability zone or subnet is required for ELB creation") - - if wait_timeout > 600: - module.fail_json(msg='wait_timeout maximum is 600 seconds') - - if security_group_names: - security_group_ids = [] - try: - ec2 = connect_to_aws(boto.ec2, region, **aws_connect_params) - if subnets: # We have at least one subnet, ergo this is a VPC - vpc_conn = _get_vpc_connection(module=module, region=region, aws_connect_params=aws_connect_params) - vpc_id = vpc_conn.get_all_subnets([subnets[0]])[0].vpc_id - filters = {'vpc_id': vpc_id} - else: - filters = None - grp_details = ec2.get_all_security_groups(filters=filters) - - for group_name in security_group_names: - if isinstance(group_name, string_types): - group_name = [group_name] - - group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name] - security_group_ids.extend(group_id) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) - - elb_man = ElbManager(module, name, listeners, purge_listeners, zones, - purge_zones, security_group_ids, health_check, - subnets, purge_subnets, scheme, - connection_draining_timeout, idle_timeout, - cross_az_load_balancing, - access_logs, stickiness, wait, wait_timeout, tags, - region=region, instance_ids=instance_ids, purge_instance_ids=purge_instance_ids, - **aws_connect_params) - - # check for unsupported attributes for this version of boto - if cross_az_load_balancing and not elb_man._check_attribute_support('cross_zone_load_balancing'): - module.fail_json(msg="You must install boto >= 2.18.0 to use the cross_az_load_balancing attribute") - - if connection_draining_timeout and not elb_man._check_attribute_support('connection_draining'): - module.fail_json(msg="You must install boto >= 2.28.0 to use the connection_draining_timeout attribute") - - if idle_timeout and not elb_man._check_attribute_support('connecting_settings'): - module.fail_json(msg="You must install boto >= 2.33.0 to use the idle_timeout attribute") - - if state == 'present': - elb_man.ensure_ok() - elif state == 'absent': - elb_man.ensure_gone() - - ansible_facts = {'ec2_elb': 'info'} - ec2_facts_result = dict(changed=elb_man.changed, - elb=elb_man.get_info(), - ansible_facts=ansible_facts) - - module.exit_json(**ec2_facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_classic_lb_info.py b/lib/ansible/modules/cloud/amazon/elb_classic_lb_info.py deleted file mode 100644 index c32bf452ca..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_classic_lb_info.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/python -# -# This is a free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This Ansible library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: elb_classic_lb_info -short_description: Gather information about EC2 Elastic Load Balancers in AWS -description: - - Gather information about EC2 Elastic Load Balancers in AWS - - This module was called C(elb_classic_lb_facts) before Ansible 2.9. The usage did not change. -version_added: "2.0" -author: - - "Michael Schultz (@mjschultz)" - - "Fernando Jose Pando (@nand0p)" -options: - names: - description: - - List of ELB names to gather information about. Pass this option to gather information about a set of ELBs, otherwise, all ELBs are returned. - type: list -extends_documentation_fragment: - - aws - - ec2 -requirements: - - botocore - - boto3 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Output format tries to match ec2_elb_lb module input parameters - -# Gather information about all ELBs -- elb_classic_lb_info: - register: elb_info - -- debug: - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" - -# Gather information about a particular ELB -- elb_classic_lb_info: - names: frontend-prod-elb - register: elb_info - -- debug: - msg: "{{ elb_info.elbs.0.dns_name }}" - -# Gather information about a set of ELBs -- elb_classic_lb_info: - names: - - frontend-prod-elb - - backend-prod-elb - register: elb_info - -- debug: - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" - -''' - -RETURN = ''' -elbs: - description: a list of load balancers - returned: always - type: list - sample: - elbs: - - attributes: - access_log: - enabled: false - connection_draining: - enabled: true - timeout: 300 - connection_settings: - idle_timeout: 60 - cross_zone_load_balancing: - enabled: true - availability_zones: - - "us-east-1a" - - "us-east-1b" - - "us-east-1c" - - "us-east-1d" - - "us-east-1e" - backend_server_description: [] - canonical_hosted_zone_name: test-lb-XXXXXXXXXXXX.us-east-1.elb.amazonaws.com - canonical_hosted_zone_name_id: XXXXXXXXXXXXXX - created_time: 2017-08-23T18:25:03.280000+00:00 - dns_name: test-lb-XXXXXXXXXXXX.us-east-1.elb.amazonaws.com - health_check: - healthy_threshold: 10 - interval: 30 - target: HTTP:80/index.html - timeout: 5 - unhealthy_threshold: 2 - instances: [] - instances_inservice: [] - instances_inservice_count: 0 - instances_outofservice: [] - instances_outofservice_count: 0 - instances_unknownservice: [] - instances_unknownservice_count: 0 - listener_descriptions: - - listener: - instance_port: 80 - instance_protocol: HTTP - load_balancer_port: 80 - protocol: HTTP - policy_names: [] - load_balancer_name: test-lb - policies: - app_cookie_stickiness_policies: [] - lb_cookie_stickiness_policies: [] - other_policies: [] - scheme: internet-facing - security_groups: - - sg-29d13055 - source_security_group: - group_name: default - owner_alias: XXXXXXXXXXXX - subnets: - - subnet-XXXXXXXX - - subnet-XXXXXXXX - tags: {} - vpc_id: vpc-c248fda4 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ( - AWSRetry, - camel_dict_to_snake_dict, - boto3_tag_list_to_ansible_dict -) - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_elbs(connection, names): - paginator = connection.get_paginator('describe_load_balancers') - load_balancers = paginator.paginate(LoadBalancerNames=names).build_full_result().get('LoadBalancerDescriptions', []) - results = [] - - for lb in load_balancers: - description = camel_dict_to_snake_dict(lb) - name = lb['LoadBalancerName'] - instances = lb.get('Instances', []) - description['tags'] = get_tags(connection, name) - description['instances_inservice'], description['instances_inservice_count'] = lb_instance_health(connection, name, instances, 'InService') - description['instances_outofservice'], description['instances_outofservice_count'] = lb_instance_health(connection, name, instances, 'OutOfService') - description['instances_unknownservice'], description['instances_unknownservice_count'] = lb_instance_health(connection, name, instances, 'Unknown') - description['attributes'] = get_lb_attributes(connection, name) - results.append(description) - return results - - -def get_lb_attributes(connection, name): - attributes = connection.describe_load_balancer_attributes(LoadBalancerName=name).get('LoadBalancerAttributes', {}) - return camel_dict_to_snake_dict(attributes) - - -def get_tags(connection, load_balancer_name): - tags = connection.describe_tags(LoadBalancerNames=[load_balancer_name])['TagDescriptions'] - if not tags: - return {} - return boto3_tag_list_to_ansible_dict(tags[0]['Tags']) - - -def lb_instance_health(connection, load_balancer_name, instances, state): - instance_states = connection.describe_instance_health(LoadBalancerName=load_balancer_name, Instances=instances).get('InstanceStates', []) - instate = [instance['InstanceId'] for instance in instance_states if instance['State'] == state] - return instate, len(instate) - - -def main(): - argument_spec = dict( - names={'default': [], 'type': 'list'} - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'elb_classic_lb_facts': - module.deprecate("The 'elb_classic_lb_facts' module has been renamed to 'elb_classic_lb_info'", version='2.13') - - connection = module.client('elb') - - try: - elbs = list_elbs(connection, module.params.get('names')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get load balancer information.") - - module.exit_json(elbs=elbs) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_instance.py b/lib/ansible/modules/cloud/amazon/elb_instance.py deleted file mode 100644 index bf2ed35f5b..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_instance.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: elb_instance -short_description: De-registers or registers instances from EC2 ELBs -description: - - This module de-registers or registers an AWS EC2 instance from the ELBs - that it belongs to. - - Returns fact "ec2_elbs" which is a list of elbs attached to the instance - if state=absent is passed as an argument. - - Will be marked changed when called only if there are ELBs found to operate on. -version_added: "1.2" -author: "John Jarvis (@jarv)" -options: - state: - description: - - register or deregister the instance - required: true - choices: ['present', 'absent'] - type: str - instance_id: - description: - - EC2 Instance ID - required: true - type: str - ec2_elbs: - description: - - List of ELB names, required for registration. The ec2_elbs fact should be used if there was a previous de-register. - type: list - enable_availability_zone: - description: - - Whether to enable the availability zone of the instance on the target ELB if the availability zone has not already - been enabled. If set to no, the task will fail if the availability zone is not enabled on the ELB. - type: bool - default: 'yes' - wait: - description: - - Wait for instance registration or deregistration to complete successfully before returning. - type: bool - default: 'yes' - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0. - type: bool - default: 'yes' - version_added: "1.5" - wait_timeout: - description: - - Number of seconds to wait for an instance to change state. If 0 then this module may return an error if a transient error occurs. - If non-zero then any transient errors are ignored until the timeout is reached. Ignored when wait=no. - default: 0 - version_added: "1.6" - type: int -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# basic pre_task and post_task example -pre_tasks: - - name: Gathering ec2 facts - action: ec2_facts - - name: Instance De-register - elb_instance: - instance_id: "{{ ansible_ec2_instance_id }}" - state: absent - delegate_to: localhost -roles: - - myrole -post_tasks: - - name: Instance Register - elb_instance: - instance_id: "{{ ansible_ec2_instance_id }}" - ec2_elbs: "{{ item }}" - state: present - delegate_to: localhost - loop: "{{ ec2_elbs }}" -""" - -import time - -try: - import boto - import boto.ec2 - import boto.ec2.autoscale - import boto.ec2.elb - from boto.regioninfo import RegionInfo - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (AnsibleAWSError, HAS_BOTO, connect_to_aws, ec2_argument_spec, - get_aws_connection_info) - - -class ElbManager: - """Handles EC2 instance ELB registration and de-registration""" - - def __init__(self, module, instance_id=None, ec2_elbs=None, - region=None, **aws_connect_params): - self.module = module - self.instance_id = instance_id - self.region = region - self.aws_connect_params = aws_connect_params - self.lbs = self._get_instance_lbs(ec2_elbs) - self.changed = False - - def deregister(self, wait, timeout): - """De-register the instance from all ELBs and wait for the ELB - to report it out-of-service""" - - for lb in self.lbs: - initial_state = self._get_instance_health(lb) - if initial_state is None: - # Instance isn't registered with this load - # balancer. Ignore it and try the next one. - continue - - lb.deregister_instances([self.instance_id]) - - # The ELB is changing state in some way. Either an instance that's - # InService is moving to OutOfService, or an instance that's - # already OutOfService is being deregistered. - self.changed = True - - if wait: - self._await_elb_instance_state(lb, 'OutOfService', initial_state, timeout) - - def register(self, wait, enable_availability_zone, timeout): - """Register the instance for all ELBs and wait for the ELB - to report the instance in-service""" - for lb in self.lbs: - initial_state = self._get_instance_health(lb) - - if enable_availability_zone: - self._enable_availailability_zone(lb) - - lb.register_instances([self.instance_id]) - - if wait: - self._await_elb_instance_state(lb, 'InService', initial_state, timeout) - else: - # We cannot assume no change was made if we don't wait - # to find out - self.changed = True - - def exists(self, lbtest): - """ Verify that the named ELB actually exists """ - - found = False - for lb in self.lbs: - if lb.name == lbtest: - found = True - break - return found - - def _enable_availailability_zone(self, lb): - """Enable the current instance's availability zone in the provided lb. - Returns True if the zone was enabled or False if no change was made. - lb: load balancer""" - instance = self._get_instance() - if instance.placement in lb.availability_zones: - return False - - lb.enable_zones(zones=instance.placement) - - # If successful, the new zone will have been added to - # lb.availability_zones - return instance.placement in lb.availability_zones - - def _await_elb_instance_state(self, lb, awaited_state, initial_state, timeout): - """Wait for an ELB to change state - lb: load balancer - awaited_state : state to poll for (string)""" - - wait_timeout = time.time() + timeout - while True: - instance_state = self._get_instance_health(lb) - - if not instance_state: - msg = ("The instance %s could not be put in service on %s." - " Reason: Invalid Instance") - self.module.fail_json(msg=msg % (self.instance_id, lb)) - - if instance_state.state == awaited_state: - # Check the current state against the initial state, and only set - # changed if they are different. - if (initial_state is None) or (instance_state.state != initial_state.state): - self.changed = True - break - elif self._is_instance_state_pending(instance_state): - # If it's pending, we'll skip further checks and continue waiting - pass - elif (awaited_state == 'InService' - and instance_state.reason_code == "Instance" - and time.time() >= wait_timeout): - # If the reason_code for the instance being out of service is - # "Instance" this indicates a failure state, e.g. the instance - # has failed a health check or the ELB does not have the - # instance's availability zone enabled. The exact reason why is - # described in InstantState.description. - msg = ("The instance %s could not be put in service on %s." - " Reason: %s") - self.module.fail_json(msg=msg % (self.instance_id, - lb, - instance_state.description)) - time.sleep(1) - - def _is_instance_state_pending(self, instance_state): - """ - Determines whether the instance_state is "pending", meaning there is - an operation under way to bring it in service. - """ - # This is messy, because AWS provides no way to distinguish between - # an instance that is is OutOfService because it's pending vs. OutOfService - # because it's failing health checks. So we're forced to analyze the - # description, which is likely to be brittle. - return (instance_state and 'pending' in instance_state.description) - - def _get_instance_health(self, lb): - """ - Check instance health, should return status object or None under - certain error conditions. - """ - try: - status = lb.get_instance_health([self.instance_id])[0] - except boto.exception.BotoServerError as e: - if e.error_code == 'InvalidInstance': - return None - else: - raise - return status - - def _get_instance_lbs(self, ec2_elbs=None): - """Returns a list of ELBs attached to self.instance_id - ec2_elbs: an optional list of elb names that will be used - for elb lookup instead of returning what elbs - are attached to self.instance_id""" - - if not ec2_elbs: - ec2_elbs = self._get_auto_scaling_group_lbs() - - try: - elb = connect_to_aws(boto.ec2.elb, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - elbs = [] - marker = None - while True: - try: - newelbs = elb.get_all_load_balancers(marker=marker) - marker = newelbs.next_marker - elbs.extend(newelbs) - if not marker: - break - except TypeError: - # Older version of boto do not allow for params - elbs = elb.get_all_load_balancers() - break - - if ec2_elbs: - lbs = sorted(lb for lb in elbs if lb.name in ec2_elbs) - else: - lbs = [] - for lb in elbs: - for info in lb.instances: - if self.instance_id == info.id: - lbs.append(lb) - return lbs - - def _get_auto_scaling_group_lbs(self): - """Returns a list of ELBs associated with self.instance_id - indirectly through its auto scaling group membership""" - - try: - asg = connect_to_aws(boto.ec2.autoscale, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - asg_instances = asg.get_all_autoscaling_instances([self.instance_id]) - if len(asg_instances) > 1: - self.module.fail_json(msg="Illegal state, expected one auto scaling group instance.") - - if not asg_instances: - asg_elbs = [] - else: - asg_name = asg_instances[0].group_name - - asgs = asg.get_all_groups([asg_name]) - if len(asg_instances) != 1: - self.module.fail_json(msg="Illegal state, expected one auto scaling group.") - - asg_elbs = asgs[0].load_balancers - - return asg_elbs - - def _get_instance(self): - """Returns a boto.ec2.InstanceObject for self.instance_id""" - try: - ec2 = connect_to_aws(boto.ec2, self.region, **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - return ec2.get_only_instances(instance_ids=[self.instance_id])[0] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state={'required': True, 'choices': ['present', 'absent']}, - instance_id={'required': True}, - ec2_elbs={'default': None, 'required': False, 'type': 'list'}, - enable_availability_zone={'default': True, 'required': False, 'type': 'bool'}, - wait={'required': False, 'default': True, 'type': 'bool'}, - wait_timeout={'required': False, 'default': 0, 'type': 'int'} - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - - ec2_elbs = module.params['ec2_elbs'] - wait = module.params['wait'] - enable_availability_zone = module.params['enable_availability_zone'] - timeout = module.params['wait_timeout'] - - if module.params['state'] == 'present' and 'ec2_elbs' not in module.params: - module.fail_json(msg="ELBs are required for registration") - - instance_id = module.params['instance_id'] - elb_man = ElbManager(module, instance_id, ec2_elbs, region=region, **aws_connect_params) - - if ec2_elbs is not None: - for elb in ec2_elbs: - if not elb_man.exists(elb): - msg = "ELB %s does not exist" % elb - module.fail_json(msg=msg) - - if not module.check_mode: - if module.params['state'] == 'present': - elb_man.register(wait, enable_availability_zone, timeout) - elif module.params['state'] == 'absent': - elb_man.deregister(wait, timeout) - - ansible_facts = {'ec2_elbs': [lb.name for lb in elb_man.lbs]} - ec2_facts_result = dict(changed=elb_man.changed, ansible_facts=ansible_facts) - - module.exit_json(**ec2_facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_network_lb.py b/lib/ansible/modules/cloud/amazon/elb_network_lb.py deleted file mode 100644 index 141223ce56..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_network_lb.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Rob White (@wimnat) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: elb_network_lb -short_description: Manage a Network Load Balancer -description: - - Manage an AWS Network Elastic Load Balancer. See - U(https://aws.amazon.com/blogs/aws/new-network-load-balancer-effortless-scaling-to-millions-of-requests-per-second/) for details. -version_added: "2.6" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - cross_zone_load_balancing: - description: - - Indicates whether cross-zone load balancing is enabled. - default: false - type: bool - deletion_protection: - description: - - Indicates whether deletion protection for the ELB is enabled. - default: false - type: bool - listeners: - description: - - A list of dicts containing listeners to attach to the ELB. See examples for detail of the dict required. Note that listener keys - are CamelCased. - type: list - elements: dict - suboptions: - Port: - description: The port on which the load balancer is listening. - type: int - required: true - Protocol: - description: The protocol for connections from clients to the load balancer. - type: str - required: true - Certificates: - description: The SSL server certificate. - type: list - elements: dict - suboptions: - CertificateArn: - description: The Amazon Resource Name (ARN) of the certificate. - type: str - SslPolicy: - description: The security policy that defines which ciphers and protocols are supported. - type: str - DefaultActions: - description: The default actions for the listener. - required: true - type: list - elements: dict - suboptions: - Type: - description: The type of action. - type: str - TargetGroupArn: - description: The Amazon Resource Name (ARN) of the target group. - type: str - name: - description: - - The name of the load balancer. This name must be unique within your AWS account, can have a maximum of 32 characters, must contain only alphanumeric - characters or hyphens, and must not begin or end with a hyphen. - required: true - type: str - purge_listeners: - description: - - If I(purge_listeners=true), existing listeners will be purged from the ELB to match exactly what is defined by I(listeners) parameter. - - If the I(listeners) parameter is not set then listeners will not be modified. - default: true - type: bool - purge_tags: - description: - - If I(purge_tags=true), existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. - - If the I(tags) parameter is not set then tags will not be modified. - default: true - type: bool - subnet_mappings: - description: - - A list of dicts containing the IDs of the subnets to attach to the load balancer. You can also specify the allocation ID of an Elastic IP - to attach to the load balancer. You can specify one Elastic IP address per subnet. - - This parameter is mutually exclusive with I(subnets). - type: list - elements: dict - subnets: - description: - - A list of the IDs of the subnets to attach to the load balancer. You can specify only one subnet per Availability Zone. You must specify subnets from - at least two Availability Zones. - - Required when I(state=present). - - This parameter is mutually exclusive with I(subnet_mappings). - type: list - scheme: - description: - - Internet-facing or internal load balancer. An ELB scheme can not be modified after creation. - default: internet-facing - choices: [ 'internet-facing', 'internal' ] - type: str - state: - description: - - Create or destroy the load balancer. - - The current default is C(absent). However, this behavior is inconsistent with other modules - and as such the default will change to C(present) in 2.14. - To maintain the existing behavior explicitly set I(state=absent). - choices: [ 'present', 'absent' ] - type: str - tags: - description: - - A dictionary of one or more tags to assign to the load balancer. - type: dict - wait: - description: - - Whether or not to wait for the network load balancer to reach the desired state. - type: bool - wait_timeout: - description: - - The duration in seconds to wait, used in conjunction with I(wait). - type: int -extends_documentation_fragment: - - aws - - ec2 -notes: - - Listeners are matched based on port. If a listener's port is changed then a new listener will be created. - - Listener rules are matched based on priority. If a rule's priority is changed then a new rule will be created. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an ELB and attach a listener -- elb_network_lb: - name: myelb - subnets: - - subnet-012345678 - - subnet-abcdef000 - listeners: - - Protocol: TCP # Required. The protocol for connections from clients to the load balancer (TCP, TLS, UDP or TCP_UDP) (case-sensitive). - Port: 80 # Required. The port on which the load balancer is listening. - DefaultActions: - - Type: forward # Required. Only 'forward' is accepted at this time - TargetGroupName: mytargetgroup # Required. The name of the target group - state: present - -# Create an ELB with an attached Elastic IP address -- elb_network_lb: - name: myelb - subnet_mappings: - - SubnetId: subnet-012345678 - AllocationId: eipalloc-aabbccdd - listeners: - - Protocol: TCP # Required. The protocol for connections from clients to the load balancer (TCP, TLS, UDP or TCP_UDP) (case-sensitive). - Port: 80 # Required. The port on which the load balancer is listening. - DefaultActions: - - Type: forward # Required. Only 'forward' is accepted at this time - TargetGroupName: mytargetgroup # Required. The name of the target group - state: present - -# Remove an ELB -- elb_network_lb: - name: myelb - state: absent - -''' - -RETURN = ''' -availability_zones: - description: The Availability Zones for the load balancer. - returned: when state is present - type: list - sample: "[{'subnet_id': 'subnet-aabbccddff', 'zone_name': 'ap-southeast-2a', 'load_balancer_addresses': []}]" -canonical_hosted_zone_id: - description: The ID of the Amazon Route 53 hosted zone associated with the load balancer. - returned: when state is present - type: str - sample: ABCDEF12345678 -created_time: - description: The date and time the load balancer was created. - returned: when state is present - type: str - sample: "2015-02-12T02:14:02+00:00" -deletion_protection_enabled: - description: Indicates whether deletion protection is enabled. - returned: when state is present - type: str - sample: true -dns_name: - description: The public DNS name of the load balancer. - returned: when state is present - type: str - sample: internal-my-elb-123456789.ap-southeast-2.elb.amazonaws.com -idle_timeout_timeout_seconds: - description: The idle timeout value, in seconds. - returned: when state is present - type: str - sample: 60 -ip_address_type: - description: The type of IP addresses used by the subnets for the load balancer. - returned: when state is present - type: str - sample: ipv4 -listeners: - description: Information about the listeners. - returned: when state is present - type: complex - contains: - listener_arn: - description: The Amazon Resource Name (ARN) of the listener. - returned: when state is present - type: str - sample: "" - load_balancer_arn: - description: The Amazon Resource Name (ARN) of the load balancer. - returned: when state is present - type: str - sample: "" - port: - description: The port on which the load balancer is listening. - returned: when state is present - type: int - sample: 80 - protocol: - description: The protocol for connections from clients to the load balancer. - returned: when state is present - type: str - sample: HTTPS - certificates: - description: The SSL server certificate. - returned: when state is present - type: complex - contains: - certificate_arn: - description: The Amazon Resource Name (ARN) of the certificate. - returned: when state is present - type: str - sample: "" - ssl_policy: - description: The security policy that defines which ciphers and protocols are supported. - returned: when state is present - type: str - sample: "" - default_actions: - description: The default actions for the listener. - returned: when state is present - type: str - contains: - type: - description: The type of action. - returned: when state is present - type: str - sample: "" - target_group_arn: - description: The Amazon Resource Name (ARN) of the target group. - returned: when state is present - type: str - sample: "" -load_balancer_arn: - description: The Amazon Resource Name (ARN) of the load balancer. - returned: when state is present - type: str - sample: arn:aws:elasticloadbalancing:ap-southeast-2:0123456789:loadbalancer/app/my-elb/001122334455 -load_balancer_name: - description: The name of the load balancer. - returned: when state is present - type: str - sample: my-elb -load_balancing_cross_zone_enabled: - description: Indicates whether cross-zone load balancing is enabled. - returned: when state is present - type: str - sample: true -scheme: - description: Internet-facing or internal load balancer. - returned: when state is present - type: str - sample: internal -state: - description: The state of the load balancer. - returned: when state is present - type: dict - sample: "{'code': 'active'}" -tags: - description: The tags attached to the load balancer. - returned: when state is present - type: dict - sample: "{ - 'Tag': 'Example' - }" -type: - description: The type of load balancer. - returned: when state is present - type: str - sample: network -vpc_id: - description: The ID of the VPC for the load balancer. - returned: when state is present - type: str - sample: vpc-0011223344 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict, compare_aws_tags -from ansible.module_utils.aws.elbv2 import NetworkLoadBalancer, ELBListeners, ELBListener - - -def create_or_update_elb(elb_obj): - """Create ELB or modify main attributes. json_exit here""" - - if elb_obj.elb: - # ELB exists so check subnets, security groups and tags match what has been passed - - # Subnets - if not elb_obj.compare_subnets(): - elb_obj.modify_subnets() - - # Tags - only need to play with tags if tags parameter has been set to something - if elb_obj.tags is not None: - - # Delete necessary tags - tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(elb_obj.elb['tags']), - boto3_tag_list_to_ansible_dict(elb_obj.tags), elb_obj.purge_tags) - if tags_to_delete: - elb_obj.delete_tags(tags_to_delete) - - # Add/update tags - if tags_need_modify: - elb_obj.modify_tags() - - else: - # Create load balancer - elb_obj.create_elb() - - # ELB attributes - elb_obj.update_elb_attributes() - elb_obj.modify_elb_attributes() - - # Listeners - listeners_obj = ELBListeners(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn']) - - listeners_to_add, listeners_to_modify, listeners_to_delete = listeners_obj.compare_listeners() - - # Delete listeners - for listener_to_delete in listeners_to_delete: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_delete, elb_obj.elb['LoadBalancerArn']) - listener_obj.delete() - listeners_obj.changed = True - - # Add listeners - for listener_to_add in listeners_to_add: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_add, elb_obj.elb['LoadBalancerArn']) - listener_obj.add() - listeners_obj.changed = True - - # Modify listeners - for listener_to_modify in listeners_to_modify: - listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_modify, elb_obj.elb['LoadBalancerArn']) - listener_obj.modify() - listeners_obj.changed = True - - # If listeners changed, mark ELB as changed - if listeners_obj.changed: - elb_obj.changed = True - - # Get the ELB again - elb_obj.update() - - # Get the ELB listeners again - listeners_obj.update() - - # Update the ELB attributes - elb_obj.update_elb_attributes() - - # Convert to snake_case and merge in everything we want to return to the user - snaked_elb = camel_dict_to_snake_dict(elb_obj.elb) - snaked_elb.update(camel_dict_to_snake_dict(elb_obj.elb_attributes)) - snaked_elb['listeners'] = [] - for listener in listeners_obj.current_listeners: - snaked_elb['listeners'].append(camel_dict_to_snake_dict(listener)) - - # Change tags to ansible friendly dict - snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(snaked_elb['tags']) - - elb_obj.module.exit_json(changed=elb_obj.changed, **snaked_elb) - - -def delete_elb(elb_obj): - - if elb_obj.elb: - elb_obj.delete() - - elb_obj.module.exit_json(changed=elb_obj.changed) - - -def main(): - - argument_spec = ( - dict( - cross_zone_load_balancing=dict(type='bool'), - deletion_protection=dict(type='bool'), - listeners=dict(type='list', - elements='dict', - options=dict( - Protocol=dict(type='str', required=True), - Port=dict(type='int', required=True), - SslPolicy=dict(type='str'), - Certificates=dict(type='list'), - DefaultActions=dict(type='list', required=True) - ) - ), - name=dict(required=True, type='str'), - purge_listeners=dict(default=True, type='bool'), - purge_tags=dict(default=True, type='bool'), - subnets=dict(type='list'), - subnet_mappings=dict(type='list'), - scheme=dict(default='internet-facing', choices=['internet-facing', 'internal']), - state=dict(choices=['present', 'absent'], type='str'), - tags=dict(type='dict'), - wait_timeout=dict(type='int'), - wait=dict(type='bool') - ) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - mutually_exclusive=[['subnets', 'subnet_mappings']]) - - # Check for subnets or subnet_mappings if state is present - state = module.params.get("state") - if state == 'present': - if module.params.get("subnets") is None and module.params.get("subnet_mappings") is None: - module.fail_json(msg="'subnets' or 'subnet_mappings' is required when state=present") - - if state is None: - # See below, unless state==present we delete. Ouch. - module.deprecate('State currently defaults to absent. This is inconsistent with other modules' - ' and the default will be changed to `present` in Ansible 2.14', - version='2.14') - - # Quick check of listeners parameters - listeners = module.params.get("listeners") - if listeners is not None: - for listener in listeners: - for key in listener.keys(): - protocols_list = ['TCP', 'TLS', 'UDP', 'TCP_UDP'] - if key == 'Protocol' and listener[key] not in protocols_list: - module.fail_json(msg="'Protocol' must be either " + ", ".join(protocols_list)) - - connection = module.client('elbv2') - connection_ec2 = module.client('ec2') - - elb = NetworkLoadBalancer(connection, connection_ec2, module) - - if state == 'present': - create_or_update_elb(elb) - else: - delete_elb(elb) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_target.py b/lib/ansible/modules/cloud/amazon/elb_target.py deleted file mode 100644 index acb7c590dd..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_target.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: elb_target -short_description: Manage a target in a target group -description: - - Used to register or deregister a target in a target group -version_added: "2.5" -author: "Rob White (@wimnat)" -options: - deregister_unused: - description: - - The default behaviour for targets that are unused is to leave them registered. If instead you would like to remove them - set I(deregister_unused) to yes. - type: bool - target_az: - description: - - An Availability Zone or all. This determines whether the target receives traffic from the load balancer nodes in the specified - Availability Zone or from all enabled Availability Zones for the load balancer. This parameter is not supported if the target - type of the target group is instance. - type: str - target_group_arn: - description: - - The Amazon Resource Name (ARN) of the target group. Mutually exclusive of I(target_group_name). - type: str - target_group_name: - description: - - The name of the target group. Mutually exclusive of I(target_group_arn). - type: str - target_id: - description: - - The ID of the target. - required: true - type: str - target_port: - description: - - The port on which the target is listening. You can specify a port override. If a target is already registered, - you can register it again using a different port. - - The default port for a target is the port for the target group. - required: false - type: int - target_status: - description: - - Blocks and waits for the target status to equal given value. For more detail on target status see - U(https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html#target-health-states) - required: false - choices: [ 'initial', 'healthy', 'unhealthy', 'unused', 'draining', 'unavailable' ] - type: str - target_status_timeout: - description: - - Maximum time in seconds to wait for target_status change - required: false - default: 60 - type: int - state: - description: - - Register or deregister the target. - required: true - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -notes: - - If you specified a port override when you registered a target, you must specify both the target ID and the port when you deregister it. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Register an IP address target to a target group -- elb_target: - target_group_name: myiptargetgroup - target_id: i-1234567 - state: present - -# Register an instance target to a target group -- elb_target: - target_group_name: mytargetgroup - target_id: i-1234567 - state: present - -# Deregister a target from a target group -- elb_target: - target_group_name: mytargetgroup - target_id: i-1234567 - state: absent - -# Modify a target to use a different port -# Register a target to a target group -- elb_target: - target_group_name: mytargetgroup - target_id: i-1234567 - target_port: 8080 - state: present - -''' - -RETURN = ''' - -''' - -import traceback -from time import time, sleep -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info, - AWSRetry) - -try: - import boto3 - from botocore.exceptions import ClientError, BotoCoreError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - - -@AWSRetry.jittered_backoff(retries=10, delay=10, catch_extra_error_codes=['TargetGroupNotFound']) -def describe_target_groups_with_backoff(connection, tg_name): - return connection.describe_target_groups(Names=[tg_name]) - - -def convert_tg_name_to_arn(connection, module, tg_name): - - try: - response = describe_target_groups_with_backoff(connection, tg_name) - except ClientError as e: - module.fail_json(msg="Unable to describe target group {0}: {1}".format(tg_name, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except BotoCoreError as e: - module.fail_json(msg="Unable to describe target group {0}: {1}".format(tg_name, to_native(e)), - exception=traceback.format_exc()) - - tg_arn = response['TargetGroups'][0]['TargetGroupArn'] - - return tg_arn - - -@AWSRetry.jittered_backoff(retries=10, delay=10, catch_extra_error_codes=['TargetGroupNotFound']) -def describe_targets_with_backoff(connection, tg_arn, target): - if target is None: - tg = [] - else: - tg = [target] - - return connection.describe_target_health(TargetGroupArn=tg_arn, Targets=tg) - - -def describe_targets(connection, module, tg_arn, target=None): - - """ - Describe targets in a target group - - :param module: ansible module object - :param connection: boto3 connection - :param tg_arn: target group arn - :param target: dictionary containing target id and port - :return: - """ - - try: - targets = describe_targets_with_backoff(connection, tg_arn, target)['TargetHealthDescriptions'] - if not targets: - return {} - return targets[0] - except ClientError as e: - module.fail_json(msg="Unable to describe target health for target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except BotoCoreError as e: - module.fail_json(msg="Unable to describe target health for target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc()) - - -@AWSRetry.jittered_backoff(retries=10, delay=10) -def register_target_with_backoff(connection, target_group_arn, target): - connection.register_targets(TargetGroupArn=target_group_arn, Targets=[target]) - - -def register_target(connection, module): - - """ - Registers a target to a target group - - :param module: ansible module object - :param connection: boto3 connection - :return: - """ - - target_az = module.params.get("target_az") - target_group_arn = module.params.get("target_group_arn") - target_id = module.params.get("target_id") - target_port = module.params.get("target_port") - target_status = module.params.get("target_status") - target_status_timeout = module.params.get("target_status_timeout") - changed = False - - if not target_group_arn: - target_group_arn = convert_tg_name_to_arn(connection, module, module.params.get("target_group_name")) - - target = dict(Id=target_id) - if target_az: - target['AvailabilityZone'] = target_az - if target_port: - target['Port'] = target_port - - target_description = describe_targets(connection, module, target_group_arn, target) - - if 'Reason' in target_description['TargetHealth']: - if target_description['TargetHealth']['Reason'] == "Target.NotRegistered": - try: - register_target_with_backoff(connection, target_group_arn, target) - changed = True - if target_status: - target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout) - except ClientError as e: - module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except BotoCoreError as e: - module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc()) - - # Get all targets for the target group - target_descriptions = describe_targets(connection, module, target_group_arn) - - module.exit_json(changed=changed, target_health_descriptions=camel_dict_to_snake_dict(target_descriptions), target_group_arn=target_group_arn) - - -@AWSRetry.jittered_backoff(retries=10, delay=10) -def deregister_target_with_backoff(connection, target_group_arn, target): - connection.deregister_targets(TargetGroupArn=target_group_arn, Targets=[target]) - - -def deregister_target(connection, module): - - """ - Deregisters a target to a target group - - :param module: ansible module object - :param connection: boto3 connection - :return: - """ - - deregister_unused = module.params.get("deregister_unused") - target_group_arn = module.params.get("target_group_arn") - target_id = module.params.get("target_id") - target_port = module.params.get("target_port") - target_status = module.params.get("target_status") - target_status_timeout = module.params.get("target_status_timeout") - changed = False - - if not target_group_arn: - target_group_arn = convert_tg_name_to_arn(connection, module, module.params.get("target_group_name")) - - target = dict(Id=target_id) - if target_port: - target['Port'] = target_port - - target_description = describe_targets(connection, module, target_group_arn, target) - current_target_state = target_description['TargetHealth']['State'] - current_target_reason = target_description['TargetHealth'].get('Reason') - - needs_deregister = False - - if deregister_unused and current_target_state == 'unused': - if current_target_reason != 'Target.NotRegistered': - needs_deregister = True - elif current_target_state not in ['unused', 'draining']: - needs_deregister = True - - if needs_deregister: - try: - deregister_target_with_backoff(connection, target_group_arn, target) - changed = True - except ClientError as e: - module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except BotoCoreError as e: - module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)), - exception=traceback.format_exc()) - else: - if current_target_reason != 'Target.NotRegistered' and current_target_state != 'draining': - module.warn(warning="Your specified target has an 'unused' state but is still registered to the target group. " + - "To force deregistration use the 'deregister_unused' option.") - - if target_status: - target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout) - - # Get all targets for the target group - target_descriptions = describe_targets(connection, module, target_group_arn) - - module.exit_json(changed=changed, target_health_descriptions=camel_dict_to_snake_dict(target_descriptions), target_group_arn=target_group_arn) - - -def target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout): - reached_state = False - timeout = target_status_timeout + time() - while time() < timeout: - health_state = describe_targets(connection, module, target_group_arn, target)['TargetHealth']['State'] - if health_state == target_status: - reached_state = True - break - sleep(1) - if not reached_state: - module.fail_json(msg='Status check timeout of {0} exceeded, last status was {1}: '.format(target_status_timeout, health_state)) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - deregister_unused=dict(type='bool', default=False), - target_az=dict(type='str'), - target_group_arn=dict(type='str'), - target_group_name=dict(type='str'), - target_id=dict(type='str', required=True), - target_port=dict(type='int'), - target_status=dict(choices=['initial', 'healthy', 'unhealthy', 'unused', 'draining', 'unavailable'], type='str'), - target_status_timeout=dict(type='int', default=60), - state=dict(required=True, choices=['present', 'absent'], type='str'), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[['target_group_arn', 'target_group_name']] - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params) - - state = module.params.get("state") - - if state == 'present': - register_target(connection, module) - else: - deregister_target(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_target_group.py b/lib/ansible/modules/cloud/amazon/elb_target_group.py deleted file mode 100644 index d8d85a2bf6..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_target_group.py +++ /dev/null @@ -1,860 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: elb_target_group -short_description: Manage a target group for an Application or Network load balancer -description: - - Manage an AWS Elastic Load Balancer target group. See - U(https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html) or - U(https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) for details. -version_added: "2.4" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - deregistration_delay_timeout: - description: - - The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused. - The range is 0-3600 seconds. - type: int - health_check_protocol: - description: - - The protocol the load balancer uses when performing health checks on targets. - required: false - choices: [ 'http', 'https', 'tcp', 'tls', 'udp', 'tcp_udp', 'HTTP', 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP'] - type: str - health_check_port: - description: - - The port the load balancer uses when performing health checks on targets. - Can be set to 'traffic-port' to match target port. - - When not defined will default to the port on which each target receives traffic from the load balancer. - required: false - type: str - health_check_path: - description: - - The ping path that is the destination on the targets for health checks. The path must be defined in order to set a health check. - - Requires the I(health_check_protocol) parameter to be set. - required: false - type: str - health_check_interval: - description: - - The approximate amount of time, in seconds, between health checks of an individual target. - required: false - type: int - health_check_timeout: - description: - - The amount of time, in seconds, during which no response from a target means a failed health check. - required: false - type: int - healthy_threshold_count: - description: - - The number of consecutive health checks successes required before considering an unhealthy target healthy. - required: false - type: int - modify_targets: - description: - - Whether or not to alter existing targets in the group to match what is passed with the module - required: false - default: yes - type: bool - name: - description: - - The name of the target group. - required: true - type: str - port: - description: - - The port on which the targets receive traffic. This port is used unless you specify a port override when registering the target. Required if - I(state) is C(present). - required: false - type: int - protocol: - description: - - The protocol to use for routing traffic to the targets. Required when I(state) is C(present). - required: false - choices: [ 'http', 'https', 'tcp', 'tls', 'udp', 'tcp_udp', 'HTTP', 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP'] - type: str - purge_tags: - description: - - If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. If the tag parameter is not set then - tags will not be modified. - required: false - default: yes - type: bool - state: - description: - - Create or destroy the target group. - required: true - choices: [ 'present', 'absent' ] - type: str - stickiness_enabled: - description: - - Indicates whether sticky sessions are enabled. - type: bool - stickiness_lb_cookie_duration: - description: - - The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load - balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). - type: int - stickiness_type: - description: - - The type of sticky sessions. The possible value is lb_cookie. - default: lb_cookie - type: str - successful_response_codes: - description: - - The HTTP codes to use when checking for a successful response from a target. - - Accepts multiple values (for example, "200,202") or a range of values (for example, "200-299"). - - Requires the I(health_check_protocol) parameter to be set. - required: false - type: str - tags: - description: - - A dictionary of one or more tags to assign to the target group. - required: false - type: dict - target_type: - description: - - The type of target that you must specify when registering targets with this target group. The possible values are - C(instance) (targets are specified by instance ID), C(ip) (targets are specified by IP address) or C(lambda) (target is specified by ARN). - Note that you can't specify targets for a target group using more than one type. Target type lambda only accept one target. When more than - one target is specified, only the first one is used. All additional targets are ignored. - If the target type is ip, specify IP addresses from the subnets of the virtual private cloud (VPC) for the target - group, the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and the RFC 6598 range (100.64.0.0/10). - You can't specify publicly routable IP addresses. - - The default behavior is C(instance). - required: false - choices: ['instance', 'ip', 'lambda'] - version_added: 2.5 - type: str - targets: - description: - - A list of targets to assign to the target group. This parameter defaults to an empty list. Unless you set the 'modify_targets' parameter then - all existing targets will be removed from the group. The list should be an Id and a Port parameter. See the Examples for detail. - required: false - type: list - unhealthy_threshold_count: - description: - - The number of consecutive health check failures required before considering a target unhealthy. - required: false - type: int - vpc_id: - description: - - The identifier of the virtual private cloud (VPC). Required when I(state) is C(present). - required: false - type: str - wait: - description: - - Whether or not to wait for the target group. - type: bool - default: false - version_added: "2.4" - wait_timeout: - description: - - The time to wait for the target group. - default: 200 - version_added: "2.4" - type: int -extends_documentation_fragment: - - aws - - ec2 -notes: - - Once a target group has been created, only its health check can then be modified using subsequent calls -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create a target group with a default health check -- elb_target_group: - name: mytargetgroup - protocol: http - port: 80 - vpc_id: vpc-01234567 - state: present - -# Modify the target group with a custom health check -- elb_target_group: - name: mytargetgroup - protocol: http - port: 80 - vpc_id: vpc-01234567 - health_check_protocol: http - health_check_path: /health_check - health_check_port: 80 - successful_response_codes: 200 - health_check_interval: 15 - health_check_timeout: 3 - healthy_threshold_count: 4 - unhealthy_threshold_count: 3 - state: present - -# Delete a target group -- elb_target_group: - name: mytargetgroup - state: absent - -# Create a target group with instance targets -- elb_target_group: - name: mytargetgroup - protocol: http - port: 81 - vpc_id: vpc-01234567 - health_check_protocol: http - health_check_path: / - successful_response_codes: "200,250-260" - targets: - - Id: i-01234567 - Port: 80 - - Id: i-98765432 - Port: 80 - state: present - wait_timeout: 200 - wait: True - -# Create a target group with IP address targets -- elb_target_group: - name: mytargetgroup - protocol: http - port: 81 - vpc_id: vpc-01234567 - health_check_protocol: http - health_check_path: / - successful_response_codes: "200,250-260" - target_type: ip - targets: - - Id: 10.0.0.10 - Port: 80 - AvailabilityZone: all - - Id: 10.0.0.20 - Port: 80 - state: present - wait_timeout: 200 - wait: True - -# Using lambda as targets require that the target group -# itself is allow to invoke the lambda function. -# therefore you need first to create an empty target group -# to receive its arn, second, allow the target group -# to invoke the lamba function and third, add the target -# to the target group -- name: first, create empty target group - elb_target_group: - name: my-lambda-targetgroup - target_type: lambda - state: present - modify_targets: False - register: out - -- name: second, allow invoke of the lambda - lambda_policy: - state: "{{ state | default('present') }}" - function_name: my-lambda-function - statement_id: someID - action: lambda:InvokeFunction - principal: elasticloadbalancing.amazonaws.com - source_arn: "{{ out.target_group_arn }}" - -- name: third, add target - elb_target_group: - name: my-lambda-targetgroup - target_type: lambda - state: present - targets: - - Id: arn:aws:lambda:eu-central-1:123456789012:function:my-lambda-function - -''' - -RETURN = ''' -deregistration_delay_timeout_seconds: - description: The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused. - returned: when state present - type: int - sample: 300 -health_check_interval_seconds: - description: The approximate amount of time, in seconds, between health checks of an individual target. - returned: when state present - type: int - sample: 30 -health_check_path: - description: The destination for the health check request. - returned: when state present - type: str - sample: /index.html -health_check_port: - description: The port to use to connect with the target. - returned: when state present - type: str - sample: traffic-port -health_check_protocol: - description: The protocol to use to connect with the target. - returned: when state present - type: str - sample: HTTP -health_check_timeout_seconds: - description: The amount of time, in seconds, during which no response means a failed health check. - returned: when state present - type: int - sample: 5 -healthy_threshold_count: - description: The number of consecutive health checks successes required before considering an unhealthy target healthy. - returned: when state present - type: int - sample: 5 -load_balancer_arns: - description: The Amazon Resource Names (ARN) of the load balancers that route traffic to this target group. - returned: when state present - type: list - sample: [] -matcher: - description: The HTTP codes to use when checking for a successful response from a target. - returned: when state present - type: dict - sample: { - "http_code": "200" - } -port: - description: The port on which the targets are listening. - returned: when state present - type: int - sample: 80 -protocol: - description: The protocol to use for routing traffic to the targets. - returned: when state present - type: str - sample: HTTP -stickiness_enabled: - description: Indicates whether sticky sessions are enabled. - returned: when state present - type: bool - sample: true -stickiness_lb_cookie_duration_seconds: - description: The time period, in seconds, during which requests from a client should be routed to the same target. - returned: when state present - type: int - sample: 86400 -stickiness_type: - description: The type of sticky sessions. - returned: when state present - type: str - sample: lb_cookie -tags: - description: The tags attached to the target group. - returned: when state present - type: dict - sample: "{ - 'Tag': 'Example' - }" -target_group_arn: - description: The Amazon Resource Name (ARN) of the target group. - returned: when state present - type: str - sample: "arn:aws:elasticloadbalancing:ap-southeast-2:01234567890:targetgroup/mytargetgroup/aabbccddee0044332211" -target_group_name: - description: The name of the target group. - returned: when state present - type: str - sample: mytargetgroup -unhealthy_threshold_count: - description: The number of consecutive health check failures required before considering the target unhealthy. - returned: when state present - type: int - sample: 2 -vpc_id: - description: The ID of the VPC for the targets. - returned: when state present - type: str - sample: vpc-0123456 -''' - -import time - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict, - compare_aws_tags, ansible_dict_to_boto3_tag_list) -from distutils.version import LooseVersion - - -def get_tg_attributes(connection, module, tg_arn): - try: - tg_attributes = boto3_tag_list_to_ansible_dict(connection.describe_target_group_attributes(TargetGroupArn=tg_arn)['Attributes']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get target group attributes") - - # Replace '.' with '_' in attribute key names to make it more Ansibley - return dict((k.replace('.', '_'), v) for k, v in tg_attributes.items()) - - -def get_target_group_tags(connection, module, target_group_arn): - try: - return connection.describe_tags(ResourceArns=[target_group_arn])['TagDescriptions'][0]['Tags'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get target group tags") - - -def get_target_group(connection, module): - try: - target_group_paginator = connection.get_paginator('describe_target_groups') - return (target_group_paginator.paginate(Names=[module.params.get("name")]).build_full_result())['TargetGroups'][0] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - if e.response['Error']['Code'] == 'TargetGroupNotFound': - return None - else: - module.fail_json_aws(e, msg="Couldn't get target group") - - -def wait_for_status(connection, module, target_group_arn, targets, status): - polling_increment_secs = 5 - max_retries = (module.params.get('wait_timeout') // polling_increment_secs) - status_achieved = False - - for x in range(0, max_retries): - try: - response = connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=targets) - if response['TargetHealthDescriptions'][0]['TargetHealth']['State'] == status: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't describe target health") - - result = response - return status_achieved, result - - -def fail_if_ip_target_type_not_supported(module): - if LooseVersion(botocore.__version__) < LooseVersion('1.7.2'): - module.fail_json(msg="target_type ip requires botocore version 1.7.2 or later. Version %s is installed" % - botocore.__version__) - - -def create_or_update_target_group(connection, module): - - changed = False - new_target_group = False - params = dict() - target_type = module.params.get("target_type") - params['Name'] = module.params.get("name") - params['TargetType'] = target_type - if target_type != "lambda": - params['Protocol'] = module.params.get("protocol").upper() - params['Port'] = module.params.get("port") - params['VpcId'] = module.params.get("vpc_id") - tags = module.params.get("tags") - purge_tags = module.params.get("purge_tags") - deregistration_delay_timeout = module.params.get("deregistration_delay_timeout") - stickiness_enabled = module.params.get("stickiness_enabled") - stickiness_lb_cookie_duration = module.params.get("stickiness_lb_cookie_duration") - stickiness_type = module.params.get("stickiness_type") - - health_option_keys = [ - "health_check_path", "health_check_protocol", "health_check_interval", "health_check_timeout", - "healthy_threshold_count", "unhealthy_threshold_count", "successful_response_codes" - ] - health_options = any([module.params[health_option_key] is not None for health_option_key in health_option_keys]) - - # Set health check if anything set - if health_options: - - if module.params.get("health_check_protocol") is not None: - params['HealthCheckProtocol'] = module.params.get("health_check_protocol").upper() - - if module.params.get("health_check_port") is not None: - params['HealthCheckPort'] = module.params.get("health_check_port") - - if module.params.get("health_check_interval") is not None: - params['HealthCheckIntervalSeconds'] = module.params.get("health_check_interval") - - if module.params.get("health_check_timeout") is not None: - params['HealthCheckTimeoutSeconds'] = module.params.get("health_check_timeout") - - if module.params.get("healthy_threshold_count") is not None: - params['HealthyThresholdCount'] = module.params.get("healthy_threshold_count") - - if module.params.get("unhealthy_threshold_count") is not None: - params['UnhealthyThresholdCount'] = module.params.get("unhealthy_threshold_count") - - # Only need to check response code and path for http(s) health checks - protocol = module.params.get("health_check_protocol") - if protocol is not None and protocol.upper() in ['HTTP', 'HTTPS']: - - if module.params.get("health_check_path") is not None: - params['HealthCheckPath'] = module.params.get("health_check_path") - - if module.params.get("successful_response_codes") is not None: - params['Matcher'] = {} - params['Matcher']['HttpCode'] = module.params.get("successful_response_codes") - - # Get target type - if target_type == 'ip': - fail_if_ip_target_type_not_supported(module) - - # Get target group - tg = get_target_group(connection, module) - - if tg: - diffs = [param for param in ('Port', 'Protocol', 'VpcId') - if tg.get(param) != params.get(param)] - if diffs: - module.fail_json(msg="Cannot modify %s parameter(s) for a target group" % - ", ".join(diffs)) - # Target group exists so check health check parameters match what has been passed - health_check_params = dict() - - # Modify health check if anything set - if health_options: - - # Health check protocol - if 'HealthCheckProtocol' in params and tg['HealthCheckProtocol'] != params['HealthCheckProtocol']: - health_check_params['HealthCheckProtocol'] = params['HealthCheckProtocol'] - - # Health check port - if 'HealthCheckPort' in params and tg['HealthCheckPort'] != params['HealthCheckPort']: - health_check_params['HealthCheckPort'] = params['HealthCheckPort'] - - # Health check interval - if 'HealthCheckIntervalSeconds' in params and tg['HealthCheckIntervalSeconds'] != params['HealthCheckIntervalSeconds']: - health_check_params['HealthCheckIntervalSeconds'] = params['HealthCheckIntervalSeconds'] - - # Health check timeout - if 'HealthCheckTimeoutSeconds' in params and tg['HealthCheckTimeoutSeconds'] != params['HealthCheckTimeoutSeconds']: - health_check_params['HealthCheckTimeoutSeconds'] = params['HealthCheckTimeoutSeconds'] - - # Healthy threshold - if 'HealthyThresholdCount' in params and tg['HealthyThresholdCount'] != params['HealthyThresholdCount']: - health_check_params['HealthyThresholdCount'] = params['HealthyThresholdCount'] - - # Unhealthy threshold - if 'UnhealthyThresholdCount' in params and tg['UnhealthyThresholdCount'] != params['UnhealthyThresholdCount']: - health_check_params['UnhealthyThresholdCount'] = params['UnhealthyThresholdCount'] - - # Only need to check response code and path for http(s) health checks - if tg['HealthCheckProtocol'] in ['HTTP', 'HTTPS']: - # Health check path - if 'HealthCheckPath'in params and tg['HealthCheckPath'] != params['HealthCheckPath']: - health_check_params['HealthCheckPath'] = params['HealthCheckPath'] - - # Matcher (successful response codes) - # TODO: required and here? - if 'Matcher' in params: - current_matcher_list = tg['Matcher']['HttpCode'].split(',') - requested_matcher_list = params['Matcher']['HttpCode'].split(',') - if set(current_matcher_list) != set(requested_matcher_list): - health_check_params['Matcher'] = {} - health_check_params['Matcher']['HttpCode'] = ','.join(requested_matcher_list) - - try: - if health_check_params: - connection.modify_target_group(TargetGroupArn=tg['TargetGroupArn'], **health_check_params) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't update target group") - - # Do we need to modify targets? - if module.params.get("modify_targets"): - # get list of current target instances. I can't see anything like a describe targets in the doco so - # describe_target_health seems to be the only way to get them - try: - current_targets = connection.describe_target_health( - TargetGroupArn=tg['TargetGroupArn']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get target group health") - - if module.params.get("targets"): - - if target_type != "lambda": - params['Targets'] = module.params.get("targets") - - # Correct type of target ports - for target in params['Targets']: - target['Port'] = int(target.get('Port', module.params.get('port'))) - - current_instance_ids = [] - - for instance in current_targets['TargetHealthDescriptions']: - current_instance_ids.append(instance['Target']['Id']) - - new_instance_ids = [] - for instance in params['Targets']: - new_instance_ids.append(instance['Id']) - - add_instances = set(new_instance_ids) - set(current_instance_ids) - - if add_instances: - instances_to_add = [] - for target in params['Targets']: - if target['Id'] in add_instances: - instances_to_add.append({'Id': target['Id'], 'Port': target['Port']}) - - changed = True - try: - connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_add) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't register targets") - - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_add, 'healthy') - if not status_achieved: - module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') - - remove_instances = set(current_instance_ids) - set(new_instance_ids) - - if remove_instances: - instances_to_remove = [] - for target in current_targets['TargetHealthDescriptions']: - if target['Target']['Id'] in remove_instances: - instances_to_remove.append({'Id': target['Target']['Id'], 'Port': target['Target']['Port']}) - - changed = True - try: - connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't remove targets") - - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') - if not status_achieved: - module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') - - # register lambda target - else: - try: - changed = False - target = module.params.get("targets")[0] - if len(current_targets["TargetHealthDescriptions"]) == 0: - changed = True - else: - for item in current_targets["TargetHealthDescriptions"]: - if target["Id"] != item["Target"]["Id"]: - changed = True - break # only one target is possible with lambda - - if changed: - if target.get("Id"): - response = connection.register_targets( - TargetGroupArn=tg['TargetGroupArn'], - Targets=[ - { - "Id": target['Id'] - } - ] - ) - - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws( - e, msg="Couldn't register targets") - else: - if target_type != "lambda": - - current_instances = current_targets['TargetHealthDescriptions'] - - if current_instances: - instances_to_remove = [] - for target in current_targets['TargetHealthDescriptions']: - instances_to_remove.append({'Id': target['Target']['Id'], 'Port': target['Target']['Port']}) - - changed = True - try: - connection.deregister_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=instances_to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't remove targets") - - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], instances_to_remove, 'unused') - if not status_achieved: - module.fail_json(msg='Error waiting for target deregistration - please check the AWS console') - - # remove lambda targets - else: - changed = False - if current_targets["TargetHealthDescriptions"]: - changed = True - # only one target is possible with lambda - target_to_remove = current_targets["TargetHealthDescriptions"][0]["Target"]["Id"] - if changed: - connection.deregister_targets( - TargetGroupArn=tg['TargetGroupArn'], Targets=[{"Id": target_to_remove}]) - else: - try: - connection.create_target_group(**params) - changed = True - new_target_group = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create target group") - - tg = get_target_group(connection, module) - - if module.params.get("targets"): - if target_type != "lambda": - params['Targets'] = module.params.get("targets") - try: - connection.register_targets(TargetGroupArn=tg['TargetGroupArn'], Targets=params['Targets']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't register targets") - - if module.params.get("wait"): - status_achieved, registered_instances = wait_for_status(connection, module, tg['TargetGroupArn'], params['Targets'], 'healthy') - if not status_achieved: - module.fail_json(msg='Error waiting for target registration to be healthy - please check the AWS console') - - else: - try: - target = module.params.get("targets")[0] - response = connection.register_targets( - TargetGroupArn=tg['TargetGroupArn'], - Targets=[ - { - "Id": target["Id"] - } - ] - ) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws( - e, msg="Couldn't register targets") - - # Now set target group attributes - update_attributes = [] - - # Get current attributes - current_tg_attributes = get_tg_attributes(connection, module, tg['TargetGroupArn']) - - if deregistration_delay_timeout is not None: - if str(deregistration_delay_timeout) != current_tg_attributes['deregistration_delay_timeout_seconds']: - update_attributes.append({'Key': 'deregistration_delay.timeout_seconds', 'Value': str(deregistration_delay_timeout)}) - if stickiness_enabled is not None: - if stickiness_enabled and current_tg_attributes['stickiness_enabled'] != "true": - update_attributes.append({'Key': 'stickiness.enabled', 'Value': 'true'}) - if stickiness_lb_cookie_duration is not None: - if str(stickiness_lb_cookie_duration) != current_tg_attributes['stickiness_lb_cookie_duration_seconds']: - update_attributes.append({'Key': 'stickiness.lb_cookie.duration_seconds', 'Value': str(stickiness_lb_cookie_duration)}) - if stickiness_type is not None and "stickiness_type" in current_tg_attributes: - if stickiness_type != current_tg_attributes['stickiness_type']: - update_attributes.append({'Key': 'stickiness.type', 'Value': stickiness_type}) - - if update_attributes: - try: - connection.modify_target_group_attributes(TargetGroupArn=tg['TargetGroupArn'], Attributes=update_attributes) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - # Something went wrong setting attributes. If this target group was created during this task, delete it to leave a consistent state - if new_target_group: - connection.delete_target_group(TargetGroupArn=tg['TargetGroupArn']) - module.fail_json_aws(e, msg="Couldn't delete target group") - - # Tags - only need to play with tags if tags parameter has been set to something - if tags: - # Get tags - current_tags = get_target_group_tags(connection, module, tg['TargetGroupArn']) - - # Delete necessary tags - tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(current_tags), tags, purge_tags) - if tags_to_delete: - try: - connection.remove_tags(ResourceArns=[tg['TargetGroupArn']], TagKeys=tags_to_delete) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete tags from target group") - changed = True - - # Add/update tags - if tags_need_modify: - try: - connection.add_tags(ResourceArns=[tg['TargetGroupArn']], Tags=ansible_dict_to_boto3_tag_list(tags_need_modify)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't add tags to target group") - changed = True - - # Get the target group again - tg = get_target_group(connection, module) - - # Get the target group attributes again - tg.update(get_tg_attributes(connection, module, tg['TargetGroupArn'])) - - # Convert tg to snake_case - snaked_tg = camel_dict_to_snake_dict(tg) - - snaked_tg['tags'] = boto3_tag_list_to_ansible_dict(get_target_group_tags(connection, module, tg['TargetGroupArn'])) - - module.exit_json(changed=changed, **snaked_tg) - - -def delete_target_group(connection, module): - changed = False - tg = get_target_group(connection, module) - - if tg: - try: - connection.delete_target_group(TargetGroupArn=tg['TargetGroupArn']) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete target group") - - module.exit_json(changed=changed) - - -def main(): - protocols_list = ['http', 'https', 'tcp', 'tls', 'udp', 'tcp_udp', 'HTTP', - 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP'] - argument_spec = dict( - deregistration_delay_timeout=dict(type='int'), - health_check_protocol=dict(choices=protocols_list), - health_check_port=dict(), - health_check_path=dict(), - health_check_interval=dict(type='int'), - health_check_timeout=dict(type='int'), - healthy_threshold_count=dict(type='int'), - modify_targets=dict(default=True, type='bool'), - name=dict(required=True), - port=dict(type='int'), - protocol=dict(choices=protocols_list), - purge_tags=dict(default=True, type='bool'), - stickiness_enabled=dict(type='bool'), - stickiness_type=dict(default='lb_cookie'), - stickiness_lb_cookie_duration=dict(type='int'), - state=dict(required=True, choices=['present', 'absent']), - successful_response_codes=dict(), - tags=dict(default={}, type='dict'), - target_type=dict(choices=['instance', 'ip', 'lambda']), - targets=dict(type='list'), - unhealthy_threshold_count=dict(type='int'), - vpc_id=dict(), - wait_timeout=dict(type='int', default=200), - wait=dict(type='bool', default=False) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[ - ['target_type', 'instance', ['protocol', 'port', 'vpc_id']], - ['target_type', 'ip', ['protocol', 'port', 'vpc_id']], - ] - ) - - if module.params.get('target_type') is None: - module.params['target_type'] = 'instance' - - connection = module.client('elbv2') - - if module.params.get('state') == 'present': - create_or_update_target_group(connection, module) - else: - delete_target_group(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_target_group_info.py b/lib/ansible/modules/cloud/amazon/elb_target_group_info.py deleted file mode 100644 index 60de28e7fb..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_target_group_info.py +++ /dev/null @@ -1,329 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: elb_target_group_info -short_description: Gather information about ELB target groups in AWS -description: - - Gather information about ELB target groups in AWS - - This module was called C(elb_target_group_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -author: Rob White (@wimnat) -options: - load_balancer_arn: - description: - - The Amazon Resource Name (ARN) of the load balancer. - required: false - type: str - target_group_arns: - description: - - The Amazon Resource Names (ARN) of the target groups. - required: false - type: list - names: - description: - - The names of the target groups. - required: false - type: list - collect_targets_health: - description: - - When set to "yes", output contains targets health description - required: false - default: no - type: bool - version_added: 2.8 - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all target groups -- elb_target_group_info: - -# Gather information about the target group attached to a particular ELB -- elb_target_group_info: - load_balancer_arn: "arn:aws:elasticloadbalancing:ap-southeast-2:001122334455:loadbalancer/app/my-elb/aabbccddeeff" - -# Gather information about a target groups named 'tg1' and 'tg2' -- elb_target_group_info: - names: - - tg1 - - tg2 - -''' - -RETURN = ''' -target_groups: - description: a list of target groups - returned: always - type: complex - contains: - deregistration_delay_timeout_seconds: - description: The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused. - returned: always - type: int - sample: 300 - health_check_interval_seconds: - description: The approximate amount of time, in seconds, between health checks of an individual target. - returned: always - type: int - sample: 30 - health_check_path: - description: The destination for the health check request. - returned: always - type: str - sample: /index.html - health_check_port: - description: The port to use to connect with the target. - returned: always - type: str - sample: traffic-port - health_check_protocol: - description: The protocol to use to connect with the target. - returned: always - type: str - sample: HTTP - health_check_timeout_seconds: - description: The amount of time, in seconds, during which no response means a failed health check. - returned: always - type: int - sample: 5 - healthy_threshold_count: - description: The number of consecutive health checks successes required before considering an unhealthy target healthy. - returned: always - type: int - sample: 5 - load_balancer_arns: - description: The Amazon Resource Names (ARN) of the load balancers that route traffic to this target group. - returned: always - type: list - sample: [] - matcher: - description: The HTTP codes to use when checking for a successful response from a target. - returned: always - type: dict - sample: { - "http_code": "200" - } - port: - description: The port on which the targets are listening. - returned: always - type: int - sample: 80 - protocol: - description: The protocol to use for routing traffic to the targets. - returned: always - type: str - sample: HTTP - stickiness_enabled: - description: Indicates whether sticky sessions are enabled. - returned: always - type: bool - sample: true - stickiness_lb_cookie_duration_seconds: - description: Indicates whether sticky sessions are enabled. - returned: always - type: int - sample: 86400 - stickiness_type: - description: The type of sticky sessions. - returned: always - type: str - sample: lb_cookie - tags: - description: The tags attached to the target group. - returned: always - type: dict - sample: "{ - 'Tag': 'Example' - }" - target_group_arn: - description: The Amazon Resource Name (ARN) of the target group. - returned: always - type: str - sample: "arn:aws:elasticloadbalancing:ap-southeast-2:01234567890:targetgroup/mytargetgroup/aabbccddee0044332211" - targets_health_description: - description: Targets health description. - returned: when collect_targets_health is enabled - type: complex - contains: - health_check_port: - description: The port to check target health. - returned: always - type: str - sample: '80' - target: - description: The target metadata. - returned: always - type: complex - contains: - id: - description: The ID of the target. - returned: always - type: str - sample: i-0123456789 - port: - description: The port to use to connect with the target. - returned: always - type: int - sample: 80 - target_health: - description: The target health status. - returned: always - type: complex - contains: - state: - description: The state of the target health. - returned: always - type: str - sample: healthy - target_group_name: - description: The name of the target group. - returned: always - type: str - sample: mytargetgroup - unhealthy_threshold_count: - description: The number of consecutive health check failures required before considering the target unhealthy. - returned: always - type: int - sample: 2 - vpc_id: - description: The ID of the VPC for the targets. - returned: always - type: str - sample: vpc-0123456 -''' - -import traceback - -try: - import boto3 - from botocore.exceptions import ClientError, NoCredentialsError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -def get_target_group_attributes(connection, module, target_group_arn): - - try: - target_group_attributes = boto3_tag_list_to_ansible_dict(connection.describe_target_group_attributes(TargetGroupArn=target_group_arn)['Attributes']) - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - # Replace '.' with '_' in attribute key names to make it more Ansibley - return dict((k.replace('.', '_'), v) - for (k, v) in target_group_attributes.items()) - - -def get_target_group_tags(connection, module, target_group_arn): - - try: - return boto3_tag_list_to_ansible_dict(connection.describe_tags(ResourceArns=[target_group_arn])['TagDescriptions'][0]['Tags']) - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def get_target_group_targets_health(connection, module, target_group_arn): - - try: - return connection.describe_target_health(TargetGroupArn=target_group_arn)['TargetHealthDescriptions'] - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - -def list_target_groups(connection, module): - - load_balancer_arn = module.params.get("load_balancer_arn") - target_group_arns = module.params.get("target_group_arns") - names = module.params.get("names") - collect_targets_health = module.params.get("collect_targets_health") - - try: - target_group_paginator = connection.get_paginator('describe_target_groups') - if not load_balancer_arn and not target_group_arns and not names: - target_groups = target_group_paginator.paginate().build_full_result() - if load_balancer_arn: - target_groups = target_group_paginator.paginate(LoadBalancerArn=load_balancer_arn).build_full_result() - if target_group_arns: - target_groups = target_group_paginator.paginate(TargetGroupArns=target_group_arns).build_full_result() - if names: - target_groups = target_group_paginator.paginate(Names=names).build_full_result() - except ClientError as e: - if e.response['Error']['Code'] == 'TargetGroupNotFound': - module.exit_json(target_groups=[]) - else: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except NoCredentialsError as e: - module.fail_json(msg="AWS authentication problem. " + e.message, exception=traceback.format_exc()) - - # Get the attributes and tags for each target group - for target_group in target_groups['TargetGroups']: - target_group.update(get_target_group_attributes(connection, module, target_group['TargetGroupArn'])) - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_target_groups = [camel_dict_to_snake_dict(target_group) for target_group in target_groups['TargetGroups']] - - # Get tags for each target group - for snaked_target_group in snaked_target_groups: - snaked_target_group['tags'] = get_target_group_tags(connection, module, snaked_target_group['target_group_arn']) - if collect_targets_health: - snaked_target_group['targets_health_description'] = [camel_dict_to_snake_dict( - target) for target in get_target_group_targets_health(connection, module, snaked_target_group['target_group_arn'])] - - module.exit_json(target_groups=snaked_target_groups) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - load_balancer_arn=dict(type='str'), - target_group_arns=dict(type='list'), - names=dict(type='list'), - collect_targets_health=dict(default=False, type='bool', required=False) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[['load_balancer_arn', 'target_group_arns', 'names']], - supports_check_mode=True - ) - if module._name == 'elb_target_group_facts': - module.deprecate("The 'elb_target_group_facts' module has been renamed to 'elb_target_group_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_target_groups(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/elb_target_info.py b/lib/ansible/modules/cloud/amazon/elb_target_info.py deleted file mode 100644 index 2bf1ddf73d..0000000000 --- a/lib/ansible/modules/cloud/amazon/elb_target_info.py +++ /dev/null @@ -1,439 +0,0 @@ -#!/usr/bin/python -# Copyright: (c) 2018, Yaakov Kuperman <ykuperman@gmail.com> -# GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, print_function - -ANSIBLE_METADATA = {"metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community"} - - -DOCUMENTATION = """ ---- -module: elb_target_info -short_description: Gathers which target groups a target is associated with. -description: - - This module will search through every target group in a region to find - which ones have registered a given instance ID or IP. - - This module was called C(elb_target_facts) before Ansible 2.9. The usage did not change. - -version_added: "2.7" -author: "Yaakov Kuperman (@yaakov-github)" -options: - instance_id: - description: - - What instance ID to get information for. - type: str - required: true - get_unused_target_groups: - description: - - Whether or not to get target groups not used by any load balancers. - type: bool - default: true - -requirements: - - boto3 - - botocore -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# practical use case - dynamically deregistering and reregistering nodes - - - name: Get EC2 Metadata - action: ec2_metadata_facts - - - name: Get initial list of target groups - delegate_to: localhost - elb_target_info: - instance_id: "{{ ansible_ec2_instance_id }}" - region: "{{ ansible_ec2_placement_region }}" - register: target_info - - - name: save fact for later - set_fact: - original_tgs: "{{ target_info.instance_target_groups }}" - - - name: Deregister instance from all target groups - delegate_to: localhost - elb_target: - target_group_arn: "{{ item.0.target_group_arn }}" - target_port: "{{ item.1.target_port }}" - target_az: "{{ item.1.target_az }}" - target_id: "{{ item.1.target_id }}" - state: absent - target_status: "draining" - region: "{{ ansible_ec2_placement_region }}" - with_subelements: - - "{{ original_tgs }}" - - "targets" - - # This avoids having to wait for 'elb_target' to serially deregister each - # target group. An alternative would be to run all of the 'elb_target' - # tasks async and wait for them to finish. - - - name: wait for all targets to deregister simultaneously - delegate_to: localhost - elb_target_info: - get_unused_target_groups: false - instance_id: "{{ ansible_ec2_instance_id }}" - region: "{{ ansible_ec2_placement_region }}" - register: target_info - until: (target_info.instance_target_groups | length) == 0 - retries: 60 - delay: 10 - - - name: reregister in elbv2s - elb_target: - region: "{{ ansible_ec2_placement_region }}" - target_group_arn: "{{ item.0.target_group_arn }}" - target_port: "{{ item.1.target_port }}" - target_az: "{{ item.1.target_az }}" - target_id: "{{ item.1.target_id }}" - state: present - target_status: "initial" - with_subelements: - - "{{ original_tgs }}" - - "targets" - - # wait until all groups associated with this instance are 'healthy' or - # 'unused' - - name: wait for registration - elb_target_info: - get_unused_target_groups: false - instance_id: "{{ ansible_ec2_instance_id }}" - region: "{{ ansible_ec2_placement_region }}" - register: target_info - until: (target_info.instance_target_groups | - map(attribute='targets') | - flatten | - map(attribute='target_health') | - rejectattr('state', 'equalto', 'healthy') | - rejectattr('state', 'equalto', 'unused') | - list | - length) == 0 - retries: 61 - delay: 10 - -# using the target groups to generate AWS CLI commands to reregister the -# instance - useful in case the playbook fails mid-run and manual -# rollback is required - - name: "reregistration commands: ELBv2s" - debug: - msg: > - aws --region {{ansible_ec2_placement_region}} elbv2 - register-targets --target-group-arn {{item.target_group_arn}} - --targets{%for target in item.targets%} - Id={{target.target_id}}, - Port={{target.target_port}}{%if target.target_az%},AvailabilityZone={{target.target_az}} - {%endif%} - {%endfor%} - loop: "{{target_info.instance_target_groups}}" - -""" - -RETURN = """ -instance_target_groups: - description: a list of target groups to which the instance is registered to - returned: always - type: complex - contains: - target_group_arn: - description: The ARN of the target group - type: str - returned: always - sample: - - "arn:aws:elasticloadbalancing:eu-west-1:111111111111:targetgroup/target-group/deadbeefdeadbeef" - target_group_type: - description: Which target type is used for this group - returned: always - type: str - sample: - - ip - - instance - targets: - description: A list of targets that point to this instance ID - returned: always - type: complex - contains: - target_id: - description: the target ID referring to this instance - type: str - returned: always - sample: - - i-deadbeef - - 1.2.3.4 - target_port: - description: which port this target is listening on - type: str - returned: always - sample: - - 80 - target_az: - description: which availability zone is explicitly - associated with this target - type: str - returned: when an AZ is associated with this instance - sample: - - us-west-2a - target_health: - description: - - The target health description. - - See following link for all the possible values - U(https://boto3.readthedocs.io/en/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.describe_target_health) - returned: always - type: complex - contains: - description: - description: description of target health - returned: if I(state!=present) - sample: - - "Target desregistration is in progress" - type: str - reason: - description: reason code for target health - returned: if I(state!=healthy) - sample: - - "Target.Deregistration in progress" - type: str - state: - description: health state - returned: always - sample: - - "healthy" - - "draining" - - "initial" - - "unhealthy" - - "unused" - - "unavailable" - type: str -""" - -__metaclass__ = type - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - # we can handle the lack of boto3 based on the ec2 module - pass - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry - - -class Target(object): - """Models a target in a target group""" - def __init__(self, target_id, port, az, raw_target_health): - self.target_port = port - self.target_id = target_id - self.target_az = az - self.target_health = self.convert_target_health(raw_target_health) - - def convert_target_health(self, raw_target_health): - return camel_dict_to_snake_dict(raw_target_health) - - -class TargetGroup(object): - """Models an elbv2 target group""" - - def __init__(self, **kwargs): - self.target_group_type = kwargs["target_group_type"] - self.target_group_arn = kwargs["target_group_arn"] - # the relevant targets associated with this group - self.targets = [] - - def add_target(self, target_id, target_port, target_az, raw_target_health): - self.targets.append(Target(target_id, - target_port, - target_az, - raw_target_health)) - - def to_dict(self): - object_dict = vars(self) - object_dict["targets"] = [vars(each) for each in self.get_targets()] - return object_dict - - def get_targets(self): - return list(self.targets) - - -class TargetInfoGatherer(object): - - def __init__(self, module, instance_id, get_unused_target_groups): - self.module = module - try: - self.ec2 = self.module.client( - "ec2", - retry_decorator=AWSRetry.jittered_backoff(retries=10) - ) - except (ClientError, BotoCoreError) as e: - self.module.fail_json_aws(e, - msg="Couldn't connect to ec2" - ) - - try: - self.elbv2 = self.module.client( - "elbv2", - retry_decorator=AWSRetry.jittered_backoff(retries=10) - ) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, - msg="Could not connect to elbv2" - ) - - self.instance_id = instance_id - self.get_unused_target_groups = get_unused_target_groups - self.tgs = self._get_target_groups() - - def _get_instance_ips(self): - """Fetch all IPs associated with this instance so that we can determine - whether or not an instance is in an IP-based target group""" - try: - # get ahold of the instance in the API - reservations = self.ec2.describe_instances( - InstanceIds=[self.instance_id], - aws_retry=True - )["Reservations"] - except (BotoCoreError, ClientError) as e: - # typically this will happen if the instance doesn't exist - self.module.fail_json_aws(e, - msg="Could not get instance info" + - " for instance '%s'" % - (self.instance_id) - ) - - if len(reservations) < 1: - self.module.fail_json( - msg="Instance ID %s could not be found" % self.instance_id - ) - - instance = reservations[0]["Instances"][0] - - # IPs are represented in a few places in the API, this should - # account for all of them - ips = set() - ips.add(instance["PrivateIpAddress"]) - for nic in instance["NetworkInterfaces"]: - ips.add(nic["PrivateIpAddress"]) - for ip in nic["PrivateIpAddresses"]: - ips.add(ip["PrivateIpAddress"]) - - return list(ips) - - def _get_target_group_objects(self): - """helper function to build a list of TargetGroup objects based on - the AWS API""" - try: - paginator = self.elbv2.get_paginator( - "describe_target_groups" - ) - tg_response = paginator.paginate().build_full_result() - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, - msg="Could not describe target" + - " groups" - ) - - # build list of TargetGroup objects representing every target group in - # the system - target_groups = [] - for each_tg in tg_response["TargetGroups"]: - if not self.get_unused_target_groups and \ - len(each_tg["LoadBalancerArns"]) < 1: - # only collect target groups that actually are connected - # to LBs - continue - - target_groups.append( - TargetGroup(target_group_arn=each_tg["TargetGroupArn"], - target_group_type=each_tg["TargetType"], - ) - ) - return target_groups - - def _get_target_descriptions(self, target_groups): - """Helper function to build a list of all the target descriptions - for this target in a target group""" - # Build a list of all the target groups pointing to this instance - # based on the previous list - tgs = set() - # Loop through all the target groups - for tg in target_groups: - try: - # Get the list of targets for that target group - response = self.elbv2.describe_target_health( - TargetGroupArn=tg.target_group_arn, - aws_retry=True - ) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e, - msg="Could not describe target " + - "health for target group %s" % - tg.target_group_arn - ) - - for t in response["TargetHealthDescriptions"]: - # If the target group has this instance as a target, add to - # list. This logic also accounts for the possibility of a - # target being in the target group multiple times with - # overridden ports - if t["Target"]["Id"] == self.instance_id or \ - t["Target"]["Id"] in self.instance_ips: - - # The 'AvailabilityZone' parameter is a weird one, see the - # API docs for more. Basically it's only supposed to be - # there under very specific circumstances, so we need - # to account for that - az = t["Target"]["AvailabilityZone"] \ - if "AvailabilityZone" in t["Target"] \ - else None - - tg.add_target(t["Target"]["Id"], - t["Target"]["Port"], - az, - t["TargetHealth"]) - # since tgs is a set, each target group will be added only - # once, even though we call add on each successful match - tgs.add(tg) - return list(tgs) - - def _get_target_groups(self): - # do this first since we need the IPs later on in this function - self.instance_ips = self._get_instance_ips() - - # build list of target groups - target_groups = self._get_target_group_objects() - return self._get_target_descriptions(target_groups) - - -def main(): - argument_spec = dict( - instance_id={"required": True, "type": "str"}, - get_unused_target_groups={"required": False, - "default": True, "type": "bool"} - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - if module._name == 'elb_target_facts': - module.deprecate("The 'elb_target_facts' module has been renamed to 'elb_target_info'", version='2.13') - - instance_id = module.params["instance_id"] - get_unused_target_groups = module.params["get_unused_target_groups"] - - tg_gatherer = TargetInfoGatherer(module, - instance_id, - get_unused_target_groups - ) - - instance_target_groups = [each.to_dict() for each in tg_gatherer.tgs] - - module.exit_json(instance_target_groups=instance_target_groups) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/cloud/amazon/execute_lambda.py b/lib/ansible/modules/cloud/amazon/execute_lambda.py deleted file mode 100644 index 1d4fe4dc9c..0000000000 --- a/lib/ansible/modules/cloud/amazon/execute_lambda.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: execute_lambda -short_description: Execute an AWS Lambda function -description: - - This module executes AWS Lambda functions, allowing synchronous and asynchronous - invocation. -version_added: "2.2" -extends_documentation_fragment: - - aws - - ec2 -author: "Ryan Scott Brown (@ryansb) <ryansb@redhat.com>" -requirements: - - python >= 2.6 - - boto3 -notes: - - Async invocation will always return an empty C(output) key. - - Synchronous invocation may result in a function timeout, resulting in an - empty C(output) key. -options: - name: - description: - - The name of the function to be invoked. This can only be used for - invocations within the calling account. To invoke a function in another - account, use I(function_arn) to specify the full ARN. - type: str - function_arn: - description: - - The name of the function to be invoked - type: str - tail_log: - description: - - If I(tail_log=yes), the result of the task will include the last 4 KB - of the CloudWatch log for the function execution. Log tailing only - works if you use synchronous invocation I(wait=yes). This is usually - used for development or testing Lambdas. - type: bool - default: false - wait: - description: - - Whether to wait for the function results or not. If I(wait=no) - the task will not return any results. To wait for the Lambda function - to complete, set I(wait=yes) and the result will be available in the - I(output) key. - type: bool - default: true - dry_run: - description: - - Do not *actually* invoke the function. A C(DryRun) call will check that - the caller has permissions to call the function, especially for - checking cross-account permissions. - type: bool - default: false - version_qualifier: - description: - - Which version/alias of the function to run. This defaults to the - C(LATEST) revision, but can be set to any existing version or alias. - See U(https://docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html) - for details. - type: str - payload: - description: - - A dictionary in any form to be provided as input to the Lambda function. - default: {} - type: dict -''' - -EXAMPLES = ''' -- execute_lambda: - name: test-function - # the payload is automatically serialized and sent to the function - payload: - foo: bar - value: 8 - register: response - -# Test that you have sufficient permissions to execute a Lambda function in -# another account -- execute_lambda: - function_arn: arn:aws:lambda:us-east-1:123456789012:function/some-function - dry_run: true - -- execute_lambda: - name: test-function - payload: - foo: bar - value: 8 - wait: true - tail_log: true - register: response - # the response will have a `logs` key that will contain a log (up to 4KB) of the function execution in Lambda - -# Pass the Lambda event payload as a json file. -- execute_lambda: - name: test-function - payload: "{{ lookup('file','lambda_event.json') }}" - register: response - -- execute_lambda: - name: test-function - version_qualifier: PRODUCTION -''' - -RETURN = ''' -output: - description: Function output if wait=true and the function returns a value - returned: success - type: dict - sample: "{ 'output': 'something' }" -logs: - description: The last 4KB of the function logs. Only provided if I(tail_log) is true - type: str - returned: if I(tail_log) == true -status: - description: C(StatusCode) of API call exit (200 for synchronous invokes, 202 for async) - type: int - sample: 200 - returned: always -''' - -import base64 -import json -import traceback - -try: - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils._text import to_native - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - name=dict(), - function_arn=dict(), - wait=dict(default=True, type='bool'), - tail_log=dict(default=False, type='bool'), - dry_run=dict(default=False, type='bool'), - version_qualifier=dict(), - payload=dict(default={}, type='dict'), - )) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ - ['name', 'function_arn'], - ] - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - name = module.params.get('name') - function_arn = module.params.get('function_arn') - await_return = module.params.get('wait') - dry_run = module.params.get('dry_run') - tail_log = module.params.get('tail_log') - version_qualifier = module.params.get('version_qualifier') - payload = module.params.get('payload') - - if not HAS_BOTO3: - module.fail_json(msg='Python module "boto3" is missing, please install it') - - if not (name or function_arn): - module.fail_json(msg="Must provide either a function_arn or a name to invoke.") - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=HAS_BOTO3) - if not region: - module.fail_json(msg="The AWS region must be specified as an " - "environment variable or in the AWS credentials " - "profile.") - - try: - client = boto3_conn(module, conn_type='client', resource='lambda', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: - module.fail_json(msg="Failure connecting boto3 to AWS: %s" % to_native(e), exception=traceback.format_exc()) - - invoke_params = {} - - if await_return: - # await response - invoke_params['InvocationType'] = 'RequestResponse' - else: - # fire and forget - invoke_params['InvocationType'] = 'Event' - if dry_run or module.check_mode: - # dry_run overrides invocation type - invoke_params['InvocationType'] = 'DryRun' - - if tail_log and await_return: - invoke_params['LogType'] = 'Tail' - elif tail_log and not await_return: - module.fail_json(msg="The `tail_log` parameter is only available if " - "the invocation waits for the function to complete. " - "Set `wait` to true or turn off `tail_log`.") - else: - invoke_params['LogType'] = 'None' - - if version_qualifier: - invoke_params['Qualifier'] = version_qualifier - - if payload: - invoke_params['Payload'] = json.dumps(payload) - - if function_arn: - invoke_params['FunctionName'] = function_arn - elif name: - invoke_params['FunctionName'] = name - - try: - response = client.invoke(**invoke_params) - except botocore.exceptions.ClientError as ce: - if ce.response['Error']['Code'] == 'ResourceNotFoundException': - module.fail_json(msg="Could not find Lambda to execute. Make sure " - "the ARN is correct and your profile has " - "permissions to execute this function.", - exception=traceback.format_exc()) - module.fail_json(msg="Client-side error when invoking Lambda, check inputs and specific error", - exception=traceback.format_exc()) - except botocore.exceptions.ParamValidationError as ve: - module.fail_json(msg="Parameters to `invoke` failed to validate", - exception=traceback.format_exc()) - except Exception as e: - module.fail_json(msg="Unexpected failure while invoking Lambda function", - exception=traceback.format_exc()) - - results = { - 'logs': '', - 'status': response['StatusCode'], - 'output': '', - } - - if response.get('LogResult'): - try: - # logs are base64 encoded in the API response - results['logs'] = base64.b64decode(response.get('LogResult', '')) - except Exception as e: - module.fail_json(msg="Failed while decoding logs", exception=traceback.format_exc()) - - if invoke_params['InvocationType'] == 'RequestResponse': - try: - results['output'] = json.loads(response['Payload'].read().decode('utf8')) - except Exception as e: - module.fail_json(msg="Failed while decoding function return value", exception=traceback.format_exc()) - - if isinstance(results.get('output'), dict) and any( - [results['output'].get('stackTrace'), results['output'].get('errorMessage')]): - # AWS sends back stack traces and error messages when a function failed - # in a RequestResponse (synchronous) context. - template = ("Function executed, but there was an error in the Lambda function. " - "Message: {errmsg}, Type: {type}, Stack Trace: {trace}") - error_data = { - # format the stacktrace sent back as an array into a multiline string - 'trace': '\n'.join( - [' '.join([ - str(x) for x in line # cast line numbers to strings - ]) for line in results.get('output', {}).get('stackTrace', [])] - ), - 'errmsg': results['output'].get('errorMessage'), - 'type': results['output'].get('errorType') - } - module.fail_json(msg=template.format(**error_data), result=results) - - module.exit_json(changed=True, result=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam.py b/lib/ansible/modules/cloud/amazon/iam.py deleted file mode 100644 index 7ff7f74be1..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam.py +++ /dev/null @@ -1,873 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam -short_description: Manage IAM users, groups, roles and keys -description: - - Allows for the management of IAM users, user API keys, groups, roles. -version_added: "2.0" -options: - iam_type: - description: - - Type of IAM resource. - choices: ["user", "group", "role"] - type: str - required: true - name: - description: - - Name of IAM resource to create or identify. - required: true - type: str - new_name: - description: - - When I(state=update), will replace I(name) with I(new_name) on IAM resource. - type: str - new_path: - description: - - When I(state=update), will replace the path with new_path on the IAM resource. - type: str - state: - description: - - Whether to create, delete or update the IAM resource. Note, roles cannot be updated. - required: true - choices: [ "present", "absent", "update" ] - type: str - path: - description: - - When creating or updating, specify the desired path of the resource. - - If I(state=present), it will replace the current path to match what is passed in when they do not match. - default: "/" - type: str - trust_policy: - description: - - The inline (JSON or YAML) trust policy document that grants an entity permission to assume the role. - - Mutually exclusive with I(trust_policy_filepath). - version_added: "2.2" - type: dict - trust_policy_filepath: - description: - - The path to the trust policy document that grants an entity permission to assume the role. - - Mutually exclusive with I(trust_policy). - version_added: "2.2" - type: str - access_key_state: - description: - - When type is user, it creates, removes, deactivates or activates a user's access key(s). Note that actions apply only to keys specified. - choices: [ "create", "remove", "active", "inactive", "Create", "Remove", "Active", "Inactive"] - type: str - key_count: - description: - - When I(access_key_state=create) it will ensure this quantity of keys are present. - default: 1 - type: int - access_key_ids: - description: - - A list of the keys that you want affected by the I(access_key_state) parameter. - type: list - groups: - description: - - A list of groups the user should belong to. When I(state=update), will gracefully remove groups not listed. - type: list - password: - description: - - When I(type=user) and either I(state=present) or I(state=update), define the users login password. - - Note that this will always return 'changed'. - type: str - update_password: - default: always - choices: ['always', 'on_create'] - description: - - When to update user passwords. - - I(update_password=always) will ensure the password is set to I(password). - - I(update_password=on_create) will only set the password for newly created users. - type: str -notes: - - 'Currently boto does not support the removal of Managed Policies, the module will error out if your - user/group/role has managed policies when you try to do state=absent. They will need to be removed manually.' -author: - - "Jonathan I. Davila (@defionscode)" - - "Paul Seiffert (@seiffert)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Basic user creation example -tasks: -- name: Create two new IAM users with API keys - iam: - iam_type: user - name: "{{ item }}" - state: present - password: "{{ temp_pass }}" - access_key_state: create - loop: - - jcleese - - mpython - -# Advanced example, create two new groups and add the pre-existing user -# jdavila to both groups. -task: -- name: Create Two Groups, Mario and Luigi - iam: - iam_type: group - name: "{{ item }}" - state: present - loop: - - Mario - - Luigi - register: new_groups - -- name: - iam: - iam_type: user - name: jdavila - state: update - groups: "{{ item.created_group.group_name }}" - loop: "{{ new_groups.results }}" - -# Example of role with custom trust policy for Lambda service -- name: Create IAM role with custom trust relationship - iam: - iam_type: role - name: AAALambdaTestRole - state: present - trust_policy: - Version: '2012-10-17' - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: lambda.amazonaws.com - -''' -RETURN = ''' -role_result: - description: the IAM.role dict returned by Boto - type: str - returned: if iam_type=role and state=present - sample: { - "arn": "arn:aws:iam::A1B2C3D4E5F6:role/my-new-role", - "assume_role_policy_document": "...truncated...", - "create_date": "2017-09-02T14:32:23Z", - "path": "/", - "role_id": "AROAA1B2C3D4E5F6G7H8I", - "role_name": "my-new-role" - } -roles: - description: a list containing the name of the currently defined roles - type: list - returned: if iam_type=role and state=present - sample: [ - "my-new-role", - "my-existing-role-1", - "my-existing-role-2", - "my-existing-role-3", - "my-existing-role-...", - ] -''' - -import json -import traceback - -try: - import boto.exception - import boto.iam - import boto.iam.connection -except ImportError: - pass # Taken care of by ec2.HAS_BOTO - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO, boto_exception, connect_to_aws, ec2_argument_spec, - get_aws_connection_info) - - -def _paginate(func, attr): - ''' - paginates the results from func by continuously passing in - the returned marker if the results were truncated. this returns - an iterator over the items in the returned response. `attr` is - the name of the attribute to iterate over in the response. - ''' - finished, marker = False, None - while not finished: - res = func(marker=marker) - for item in getattr(res, attr): - yield item - - finished = res.is_truncated == 'false' - if not finished: - marker = res.marker - - -def list_all_groups(iam): - return [item['group_name'] for item in _paginate(iam.get_all_groups, 'groups')] - - -def list_all_users(iam): - return [item['user_name'] for item in _paginate(iam.get_all_users, 'users')] - - -def list_all_roles(iam): - return [item['role_name'] for item in _paginate(iam.list_roles, 'roles')] - - -def list_all_instance_profiles(iam): - return [item['instance_profile_name'] for item in _paginate(iam.list_instance_profiles, 'instance_profiles')] - - -def create_user(module, iam, name, pwd, path, key_state, key_count): - key_qty = 0 - keys = [] - try: - user_meta = iam.create_user( - name, path).create_user_response.create_user_result.user - changed = True - if pwd is not None: - pwd = iam.create_login_profile(name, pwd) - if key_state in ['create']: - if key_count: - while key_count > key_qty: - keys.append(iam.create_access_key( - user_name=name).create_access_key_response. - create_access_key_result. - access_key) - key_qty += 1 - else: - keys = None - except boto.exception.BotoServerError as err: - module.fail_json(changed=False, msg=str(err)) - else: - user_info = dict(created_user=user_meta, password=pwd, access_keys=keys) - return (user_info, changed) - - -def delete_dependencies_first(module, iam, name): - changed = False - # try to delete any keys - try: - current_keys = [ck['access_key_id'] for ck in - iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata] - for key in current_keys: - iam.delete_access_key(key, name) - changed = True - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg="Failed to delete keys: %s" % err, exception=traceback.format_exc()) - - # try to delete login profiles - try: - login_profile = iam.get_login_profiles(name).get_login_profile_response - iam.delete_login_profile(name) - changed = True - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if 'Login Profile for User ' + name + ' cannot be found.' not in error_msg: - module.fail_json(changed=changed, msg="Failed to delete login profile: %s" % err, exception=traceback.format_exc()) - - # try to detach policies - try: - for policy in iam.get_all_user_policies(name).list_user_policies_result.policy_names: - iam.delete_user_policy(name, policy) - changed = True - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if 'must detach all policies first' in error_msg: - module.fail_json(changed=changed, msg="All inline policies have been removed. Though it appears" - "that %s has Managed Polices. This is not " - "currently supported by boto. Please detach the policies " - "through the console and try again." % name) - module.fail_json(changed=changed, msg="Failed to delete policies: %s" % err, exception=traceback.format_exc()) - - # try to deactivate associated MFA devices - try: - mfa_devices = iam.get_all_mfa_devices(name).get('list_mfa_devices_response', {}).get('list_mfa_devices_result', {}).get('mfa_devices', []) - for device in mfa_devices: - iam.deactivate_mfa_device(name, device['serial_number']) - changed = True - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg="Failed to deactivate associated MFA devices: %s" % err, exception=traceback.format_exc()) - - return changed - - -def delete_user(module, iam, name): - changed = delete_dependencies_first(module, iam, name) - try: - iam.delete_user(name) - except boto.exception.BotoServerError as ex: - module.fail_json(changed=changed, msg="Failed to delete user %s: %s" % (name, ex), exception=traceback.format_exc()) - else: - changed = True - return name, changed - - -def update_user(module, iam, name, new_name, new_path, key_state, key_count, keys, pwd, updated): - changed = False - name_change = False - if updated and new_name: - name = new_name - try: - current_keys = [ck['access_key_id'] for ck in - iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata] - status = [ck['status'] for ck in - iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata] - key_qty = len(current_keys) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if 'cannot be found' in error_msg and updated: - current_keys = [ck['access_key_id'] for ck in - iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata] - status = [ck['status'] for ck in - iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata] - name = new_name - else: - module.fail_json(changed=False, msg=str(err)) - - updated_key_list = {} - - if new_name or new_path: - c_path = iam.get_user(name).get_user_result.user['path'] - if (name != new_name) or (c_path != new_path): - changed = True - try: - if not updated: - user = iam.update_user( - name, new_user_name=new_name, new_path=new_path).update_user_response.response_metadata - else: - user = iam.update_user( - name, new_path=new_path).update_user_response.response_metadata - user['updates'] = dict( - old_username=name, new_username=new_name, old_path=c_path, new_path=new_path) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - module.fail_json(changed=False, msg=str(err)) - else: - if not updated: - name_change = True - - if pwd: - try: - iam.update_login_profile(name, pwd) - changed = True - except boto.exception.BotoServerError: - try: - iam.create_login_profile(name, pwd) - changed = True - except boto.exception.BotoServerError as err: - error_msg = boto_exception(str(err)) - if 'Password does not conform to the account password policy' in error_msg: - module.fail_json(changed=False, msg="Password doesn't conform to policy") - else: - module.fail_json(msg=error_msg) - - try: - current_keys = [ck['access_key_id'] for ck in - iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata] - status = [ck['status'] for ck in - iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata] - key_qty = len(current_keys) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if 'cannot be found' in error_msg and updated: - current_keys = [ck['access_key_id'] for ck in - iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata] - status = [ck['status'] for ck in - iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata] - name = new_name - else: - module.fail_json(changed=False, msg=str(err)) - - new_keys = [] - if key_state == 'create': - try: - while key_count > key_qty: - new_keys.append(iam.create_access_key( - user_name=name).create_access_key_response.create_access_key_result.access_key) - key_qty += 1 - changed = True - - except boto.exception.BotoServerError as err: - module.fail_json(changed=False, msg=str(err)) - - if keys and key_state: - for access_key in keys: - if key_state in ('active', 'inactive'): - if access_key in current_keys: - for current_key, current_key_state in zip(current_keys, status): - if key_state != current_key_state.lower(): - try: - iam.update_access_key(access_key, key_state.capitalize(), user_name=name) - changed = True - except boto.exception.BotoServerError as err: - module.fail_json(changed=False, msg=str(err)) - else: - module.fail_json(msg="Supplied keys not found for %s. " - "Current keys: %s. " - "Supplied key(s): %s" % - (name, current_keys, keys) - ) - - if key_state == 'remove': - if access_key in current_keys: - try: - iam.delete_access_key(access_key, user_name=name) - except boto.exception.BotoServerError as err: - module.fail_json(changed=False, msg=str(err)) - else: - changed = True - - try: - final_keys, final_key_status = \ - [ck['access_key_id'] for ck in - iam.get_all_access_keys(name). - list_access_keys_result. - access_key_metadata],\ - [ck['status'] for ck in - iam.get_all_access_keys(name). - list_access_keys_result. - access_key_metadata] - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - - for fk, fks in zip(final_keys, final_key_status): - updated_key_list.update({fk: fks}) - - return name_change, updated_key_list, changed, new_keys - - -def set_users_groups(module, iam, name, groups, updated=None, - new_name=None): - """ Sets groups for a user, will purge groups not explicitly passed, while - retaining pre-existing groups that also are in the new list. - """ - changed = False - - if updated: - name = new_name - - try: - orig_users_groups = [og['group_name'] for og in iam.get_groups_for_user( - name).list_groups_for_user_result.groups] - remove_groups = [ - rg for rg in frozenset(orig_users_groups).difference(groups)] - new_groups = [ - ng for ng in frozenset(groups).difference(orig_users_groups)] - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - else: - if len(orig_users_groups) > 0: - for new in new_groups: - iam.add_user_to_group(new, name) - for rm in remove_groups: - iam.remove_user_from_group(rm, name) - else: - for group in groups: - try: - iam.add_user_to_group(group, name) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if ('The group with name %s cannot be found.' % group) in error_msg: - module.fail_json(changed=False, msg="Group %s doesn't exist" % group) - - if len(remove_groups) > 0 or len(new_groups) > 0: - changed = True - - return (groups, changed) - - -def create_group(module=None, iam=None, name=None, path=None): - changed = False - try: - iam.create_group( - name, path).create_group_response.create_group_result.group - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - else: - changed = True - return name, changed - - -def delete_group(module=None, iam=None, name=None): - changed = False - try: - iam.delete_group(name) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if ('must delete policies first') in error_msg: - for policy in iam.get_all_group_policies(name).list_group_policies_result.policy_names: - iam.delete_group_policy(name, policy) - try: - iam.delete_group(name) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if ('must delete policies first') in error_msg: - module.fail_json(changed=changed, msg="All inline policies have been removed. Though it appears" - "that %s has Managed Polices. This is not " - "currently supported by boto. Please detach the policies " - "through the console and try again." % name) - else: - module.fail_json(changed=changed, msg=str(error_msg)) - else: - changed = True - else: - module.fail_json(changed=changed, msg=str(error_msg)) - else: - changed = True - return changed, name - - -def update_group(module=None, iam=None, name=None, new_name=None, new_path=None): - changed = False - try: - current_group_path = iam.get_group( - name).get_group_response.get_group_result.group['path'] - if new_path: - if current_group_path != new_path: - iam.update_group(name, new_path=new_path) - changed = True - if new_name: - if name != new_name: - iam.update_group(name, new_group_name=new_name, new_path=new_path) - changed = True - name = new_name - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - - return changed, name, new_path, current_group_path - - -def create_role(module, iam, name, path, role_list, prof_list, trust_policy_doc): - changed = False - iam_role_result = None - instance_profile_result = None - try: - if name not in role_list: - changed = True - iam_role_result = iam.create_role(name, - assume_role_policy_document=trust_policy_doc, - path=path).create_role_response.create_role_result.role - - if name not in prof_list: - instance_profile_result = iam.create_instance_profile(name, path=path) \ - .create_instance_profile_response.create_instance_profile_result.instance_profile - iam.add_role_to_instance_profile(name, name) - else: - instance_profile_result = iam.get_instance_profile(name).get_instance_profile_response.get_instance_profile_result.instance_profile - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - else: - updated_role_list = list_all_roles(iam) - iam_role_result = iam.get_role(name).get_role_response.get_role_result.role - return changed, updated_role_list, iam_role_result, instance_profile_result - - -def delete_role(module, iam, name, role_list, prof_list): - changed = False - iam_role_result = None - instance_profile_result = None - try: - if name in role_list: - cur_ins_prof = [rp['instance_profile_name'] for rp in - iam.list_instance_profiles_for_role(name). - list_instance_profiles_for_role_result. - instance_profiles] - for profile in cur_ins_prof: - iam.remove_role_from_instance_profile(profile, name) - try: - iam.delete_role(name) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if ('must detach all policies first') in error_msg: - for policy in iam.list_role_policies(name).list_role_policies_result.policy_names: - iam.delete_role_policy(name, policy) - try: - iam_role_result = iam.delete_role(name) - except boto.exception.BotoServerError as err: - error_msg = boto_exception(err) - if ('must detach all policies first') in error_msg: - module.fail_json(changed=changed, msg="All inline policies have been removed. Though it appears" - "that %s has Managed Polices. This is not " - "currently supported by boto. Please detach the policies " - "through the console and try again." % name) - else: - module.fail_json(changed=changed, msg=str(err)) - else: - changed = True - - else: - changed = True - - for prof in prof_list: - if name == prof: - instance_profile_result = iam.delete_instance_profile(name) - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err)) - else: - updated_role_list = list_all_roles(iam) - return changed, updated_role_list, iam_role_result, instance_profile_result - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - iam_type=dict(required=True, choices=['user', 'group', 'role']), - groups=dict(type='list', default=None, required=False), - state=dict(required=True, choices=['present', 'absent', 'update']), - password=dict(default=None, required=False, no_log=True), - update_password=dict(default='always', required=False, choices=['always', 'on_create']), - access_key_state=dict(default=None, required=False, choices=[ - 'active', 'inactive', 'create', 'remove', - 'Active', 'Inactive', 'Create', 'Remove']), - access_key_ids=dict(type='list', default=None, required=False), - key_count=dict(type='int', default=1, required=False), - name=dict(required=True), - trust_policy_filepath=dict(default=None, required=False), - trust_policy=dict(type='dict', default=None, required=False), - new_name=dict(default=None, required=False), - path=dict(default='/', required=False), - new_path=dict(default=None, required=False) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['trust_policy', 'trust_policy_filepath']], - ) - - if not HAS_BOTO: - module.fail_json(msg='This module requires boto, please install it') - - state = module.params.get('state').lower() - iam_type = module.params.get('iam_type').lower() - groups = module.params.get('groups') - name = module.params.get('name') - new_name = module.params.get('new_name') - password = module.params.get('password') - update_pw = module.params.get('update_password') - path = module.params.get('path') - new_path = module.params.get('new_path') - key_count = module.params.get('key_count') - key_state = module.params.get('access_key_state') - trust_policy = module.params.get('trust_policy') - trust_policy_filepath = module.params.get('trust_policy_filepath') - key_ids = module.params.get('access_key_ids') - - if key_state: - key_state = key_state.lower() - if any([n in key_state for n in ['active', 'inactive']]) and not key_ids: - module.fail_json(changed=False, msg="At least one access key has to be defined in order" - " to use 'active' or 'inactive'") - - if iam_type == 'user' and module.params.get('password') is not None: - pwd = module.params.get('password') - elif iam_type != 'user' and module.params.get('password') is not None: - module.fail_json(msg="a password is being specified when the iam_type " - "is not user. Check parameters") - else: - pwd = None - - if iam_type != 'user' and (module.params.get('access_key_state') is not None or - module.params.get('access_key_id') is not None): - module.fail_json(msg="the IAM type must be user, when IAM access keys " - "are being modified. Check parameters") - - if iam_type == 'role' and state == 'update': - module.fail_json(changed=False, msg="iam_type: role, cannot currently be updated, " - "please specify present or absent") - - # check if trust_policy is present -- it can be inline JSON or a file path to a JSON file - if trust_policy_filepath: - try: - with open(trust_policy_filepath, 'r') as json_data: - trust_policy_doc = json.dumps(json.load(json_data)) - except Exception as e: - module.fail_json(msg=str(e) + ': ' + trust_policy_filepath) - elif trust_policy: - try: - trust_policy_doc = json.dumps(trust_policy) - except Exception as e: - module.fail_json(msg=str(e) + ': ' + trust_policy) - else: - trust_policy_doc = None - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - try: - if region: - iam = connect_to_aws(boto.iam, region, **aws_connect_kwargs) - else: - iam = boto.iam.connection.IAMConnection(**aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) - - result = {} - changed = False - - try: - orig_group_list = list_all_groups(iam) - - orig_user_list = list_all_users(iam) - - orig_role_list = list_all_roles(iam) - - orig_prof_list = list_all_instance_profiles(iam) - except boto.exception.BotoServerError as err: - module.fail_json(msg=err.message) - - if iam_type == 'user': - been_updated = False - user_groups = None - user_exists = any([n in [name, new_name] for n in orig_user_list]) - if user_exists: - current_path = iam.get_user(name).get_user_result.user['path'] - if not new_path and current_path != path: - new_path = path - path = current_path - - if state == 'present' and not user_exists and not new_name: - (meta, changed) = create_user( - module, iam, name, password, path, key_state, key_count) - keys = iam.get_all_access_keys(name).list_access_keys_result.\ - access_key_metadata - if groups: - (user_groups, changed) = set_users_groups( - module, iam, name, groups, been_updated, new_name) - module.exit_json( - user_meta=meta, groups=user_groups, keys=keys, changed=changed) - - elif state in ['present', 'update'] and user_exists: - if update_pw == 'on_create': - password = None - if name not in orig_user_list and new_name in orig_user_list: - been_updated = True - name_change, key_list, user_changed, new_key = update_user( - module, iam, name, new_name, new_path, key_state, key_count, key_ids, password, been_updated) - if new_key: - user_meta = {'access_keys': list(new_key)} - user_meta['access_keys'].extend( - [{'access_key_id': key, 'status': value} for key, value in key_list.items() if - key not in [it['access_key_id'] for it in new_key]]) - else: - user_meta = { - 'access_keys': [{'access_key_id': key, 'status': value} for key, value in key_list.items()]} - - if name_change and new_name: - orig_name = name - name = new_name - if isinstance(groups, list): - user_groups, groups_changed = set_users_groups( - module, iam, name, groups, been_updated, new_name) - if groups_changed == user_changed: - changed = groups_changed - else: - changed = True - else: - changed = user_changed - if new_name and new_path: - module.exit_json(changed=changed, groups=user_groups, old_user_name=orig_name, - new_user_name=new_name, old_path=path, new_path=new_path, keys=key_list, - created_keys=new_key, user_meta=user_meta) - elif new_name and not new_path and not been_updated: - module.exit_json( - changed=changed, groups=user_groups, old_user_name=orig_name, new_user_name=new_name, keys=key_list, - created_keys=new_key, user_meta=user_meta) - elif new_name and not new_path and been_updated: - module.exit_json( - changed=changed, groups=user_groups, user_name=new_name, keys=key_list, key_state=key_state, - created_keys=new_key, user_meta=user_meta) - elif not new_name and new_path: - module.exit_json( - changed=changed, groups=user_groups, user_name=name, old_path=path, new_path=new_path, - keys=key_list, created_keys=new_key, user_meta=user_meta) - else: - module.exit_json( - changed=changed, groups=user_groups, user_name=name, keys=key_list, created_keys=new_key, - user_meta=user_meta) - - elif state == 'update' and not user_exists: - module.fail_json( - msg="The user %s does not exist. No update made." % name) - - elif state == 'absent': - if user_exists: - try: - set_users_groups(module, iam, name, '') - name, changed = delete_user(module, iam, name) - module.exit_json(deleted_user=name, changed=changed) - - except Exception as ex: - module.fail_json(changed=changed, msg=str(ex)) - else: - module.exit_json( - changed=False, msg="User %s is already absent from your AWS IAM users" % name) - - elif iam_type == 'group': - group_exists = name in orig_group_list - - if state == 'present' and not group_exists: - new_group, changed = create_group(module=module, iam=iam, name=name, path=path) - module.exit_json(changed=changed, group_name=new_group) - elif state in ['present', 'update'] and group_exists: - changed, updated_name, updated_path, cur_path = update_group( - module=module, iam=iam, name=name, new_name=new_name, - new_path=new_path) - - if new_path and new_name: - module.exit_json(changed=changed, old_group_name=name, - new_group_name=updated_name, old_path=cur_path, - new_group_path=updated_path) - - if new_path and not new_name: - module.exit_json(changed=changed, group_name=name, - old_path=cur_path, - new_group_path=updated_path) - - if not new_path and new_name: - module.exit_json(changed=changed, old_group_name=name, - new_group_name=updated_name, group_path=cur_path) - - if not new_path and not new_name: - module.exit_json( - changed=changed, group_name=name, group_path=cur_path) - - elif state == 'update' and not group_exists: - module.fail_json( - changed=changed, msg="Update Failed. Group %s doesn't seem to exist!" % name) - - elif state == 'absent': - if name in orig_group_list: - removed_group, changed = delete_group(module=module, iam=iam, name=name) - module.exit_json(changed=changed, delete_group=removed_group) - else: - module.exit_json(changed=changed, msg="Group already absent") - - elif iam_type == 'role': - role_list = [] - if state == 'present': - changed, role_list, role_result, instance_profile_result = create_role( - module, iam, name, path, orig_role_list, orig_prof_list, trust_policy_doc) - elif state == 'absent': - changed, role_list, role_result, instance_profile_result = delete_role( - module, iam, name, orig_role_list, orig_prof_list) - elif state == 'update': - module.fail_json( - changed=False, msg='Role update not currently supported by boto.') - module.exit_json(changed=changed, roles=role_list, role_result=role_result, - instance_profile_result=instance_profile_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_cert.py b/lib/ansible/modules/cloud/amazon/iam_cert.py deleted file mode 100644 index ecdab0ae3e..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_cert.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_cert -short_description: Manage server certificates for use on ELBs and CloudFront -description: - - Allows for the management of server certificates. -version_added: "2.0" -options: - name: - description: - - Name of certificate to add, update or remove. - required: true - type: str - new_name: - description: - - When state is present, this will update the name of the cert. - - The cert, key and cert_chain parameters will be ignored if this is defined. - type: str - new_path: - description: - - When state is present, this will update the path of the cert. - - The I(cert), I(key) and I(cert_chain) parameters will be ignored if this is defined. - type: str - state: - description: - - Whether to create(or update) or delete the certificate. - - If I(new_path) or I(new_name) is defined, specifying present will attempt to make an update these. - required: true - choices: [ "present", "absent" ] - type: str - path: - description: - - When creating or updating, specify the desired path of the certificate. - default: "/" - type: str - cert_chain: - description: - - The path to, or content of, the CA certificate chain in PEM encoded format. - As of 2.4 content is accepted. If the parameter is not a file, it is assumed to be content. - type: str - cert: - description: - - The path to, or content of the certificate body in PEM encoded format. - As of 2.4 content is accepted. If the parameter is not a file, it is assumed to be content. - type: str - key: - description: - - The path to, or content of the private key in PEM encoded format. - As of 2.4 content is accepted. If the parameter is not a file, it is assumed to be content. - type: str - dup_ok: - description: - - By default the module will not upload a certificate that is already uploaded into AWS. - - If I(dup_ok=True), it will upload the certificate as long as the name is unique. - default: False - type: bool - -requirements: [ "boto" ] -author: Jonathan I. Davila (@defionscode) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Basic server certificate upload from local file -- iam_cert: - name: very_ssl - state: present - cert: "{{ lookup('file', 'path/to/cert') }}" - key: "{{ lookup('file', 'path/to/key') }}" - cert_chain: "{{ lookup('file', 'path/to/certchain') }}" - -# Basic server certificate upload -- iam_cert: - name: very_ssl - state: present - cert: path/to/cert - key: path/to/key - cert_chain: path/to/certchain - -# Server certificate upload using key string -- iam_cert: - name: very_ssl - state: present - path: "/a/cert/path/" - cert: body_of_somecert - key: vault_body_of_privcertkey - cert_chain: body_of_myverytrustedchain - -# Basic rename of existing certificate -- iam_cert: - name: very_ssl - new_name: new_very_ssl - state: present - -''' -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, connect_to_aws -import os - -try: - import boto - import boto.iam - import boto.ec2 - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - - -def cert_meta(iam, name): - certificate = iam.get_server_certificate(name).get_server_certificate_result.server_certificate - ocert = certificate.certificate_body - opath = certificate.server_certificate_metadata.path - ocert_id = certificate.server_certificate_metadata.server_certificate_id - upload_date = certificate.server_certificate_metadata.upload_date - exp = certificate.server_certificate_metadata.expiration - arn = certificate.server_certificate_metadata.arn - return opath, ocert, ocert_id, upload_date, exp, arn - - -def dup_check(module, iam, name, new_name, cert, orig_cert_names, orig_cert_bodies, dup_ok): - update = False - - # IAM cert names are case insensitive - names_lower = [n.lower() for n in [name, new_name] if n is not None] - orig_cert_names_lower = [ocn.lower() for ocn in orig_cert_names] - - if any(ct in orig_cert_names_lower for ct in names_lower): - for i_name in names_lower: - if cert is not None: - try: - c_index = orig_cert_names_lower.index(i_name) - except NameError: - continue - else: - # NOTE: remove the carriage return to strictly compare the cert bodies. - slug_cert = cert.replace('\r', '') - slug_orig_cert_bodies = orig_cert_bodies[c_index].replace('\r', '') - if slug_orig_cert_bodies == slug_cert: - update = True - break - elif slug_cert.startswith(slug_orig_cert_bodies): - update = True - break - else: - module.fail_json(changed=False, msg='A cert with the name %s already exists and' - ' has a different certificate body associated' - ' with it. Certificates cannot have the same name' % orig_cert_names[c_index]) - else: - update = True - break - elif cert in orig_cert_bodies and not dup_ok: - for crt_name, crt_body in zip(orig_cert_names, orig_cert_bodies): - if crt_body == cert: - module.fail_json(changed=False, msg='This certificate already' - ' exists under the name %s' % crt_name) - - return update - - -def cert_action(module, iam, name, cpath, new_name, new_path, state, - cert, key, cert_chain, orig_cert_names, orig_cert_bodies, dup_ok): - if state == 'present': - update = dup_check(module, iam, name, new_name, cert, orig_cert_names, - orig_cert_bodies, dup_ok) - if update: - opath, ocert, ocert_id, upload_date, exp, arn = cert_meta(iam, name) - changed = True - if new_name and new_path: - iam.update_server_cert(name, new_cert_name=new_name, new_path=new_path) - module.exit_json(changed=changed, original_name=name, new_name=new_name, - original_path=opath, new_path=new_path, cert_body=ocert, - upload_date=upload_date, expiration_date=exp, arn=arn) - elif new_name and not new_path: - iam.update_server_cert(name, new_cert_name=new_name) - module.exit_json(changed=changed, original_name=name, new_name=new_name, - cert_path=opath, cert_body=ocert, - upload_date=upload_date, expiration_date=exp, arn=arn) - elif not new_name and new_path: - iam.update_server_cert(name, new_path=new_path) - module.exit_json(changed=changed, name=new_name, - original_path=opath, new_path=new_path, cert_body=ocert, - upload_date=upload_date, expiration_date=exp, arn=arn) - else: - changed = False - module.exit_json(changed=changed, name=name, cert_path=opath, cert_body=ocert, - upload_date=upload_date, expiration_date=exp, arn=arn, - msg='No new path or name specified. No changes made') - else: - changed = True - iam.upload_server_cert(name, cert, key, cert_chain=cert_chain, path=cpath) - opath, ocert, ocert_id, upload_date, exp, arn = cert_meta(iam, name) - module.exit_json(changed=changed, name=name, cert_path=opath, cert_body=ocert, - upload_date=upload_date, expiration_date=exp, arn=arn) - elif state == 'absent': - if name in orig_cert_names: - changed = True - iam.delete_server_cert(name) - module.exit_json(changed=changed, deleted_cert=name) - else: - changed = False - module.exit_json(changed=changed, msg='Certificate with the name %s already absent' % name) - - -def load_data(cert, key, cert_chain): - # if paths are provided rather than lookups read the files and return the contents - if cert and os.path.isfile(cert): - with open(cert, 'r') as cert_fh: - cert = cert_fh.read().rstrip() - if key and os.path.isfile(key): - with open(key, 'r') as key_fh: - key = key_fh.read().rstrip() - if cert_chain and os.path.isfile(cert_chain): - with open(cert_chain, 'r') as cert_chain_fh: - cert_chain = cert_chain_fh.read() - return cert, key, cert_chain - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(required=True), - cert=dict(), - key=dict(no_log=True), - cert_chain=dict(), - new_name=dict(), - path=dict(default='/'), - new_path=dict(), - dup_ok=dict(type='bool') - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['new_path', 'key'], - ['new_path', 'cert'], - ['new_path', 'cert_chain'], - ['new_name', 'key'], - ['new_name', 'cert'], - ['new_name', 'cert_chain'], - ], - ) - - if not HAS_BOTO: - module.fail_json(msg="Boto is required for this module") - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - try: - if region: - iam = connect_to_aws(boto.iam, region, **aws_connect_kwargs) - else: - iam = boto.iam.connection.IAMConnection(**aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) - - state = module.params.get('state') - name = module.params.get('name') - path = module.params.get('path') - new_name = module.params.get('new_name') - new_path = module.params.get('new_path') - dup_ok = module.params.get('dup_ok') - if state == 'present' and not new_name and not new_path: - cert, key, cert_chain = load_data(cert=module.params.get('cert'), - key=module.params.get('key'), - cert_chain=module.params.get('cert_chain')) - else: - cert = key = cert_chain = None - - orig_cert_names = [ctb['server_certificate_name'] for ctb in - iam.get_all_server_certs().list_server_certificates_result.server_certificate_metadata_list] - orig_cert_bodies = [iam.get_server_certificate(thing).get_server_certificate_result.certificate_body - for thing in orig_cert_names] - if new_name == name: - new_name = None - if new_path == path: - new_path = None - - changed = False - try: - cert_action(module, iam, name, path, new_name, new_path, state, - cert, key, cert_chain, orig_cert_names, orig_cert_bodies, dup_ok) - except boto.exception.BotoServerError as err: - module.fail_json(changed=changed, msg=str(err), debug=[cert, key]) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_group.py b/lib/ansible/modules/cloud/amazon/iam_group.py deleted file mode 100644 index 68327e68fb..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_group.py +++ /dev/null @@ -1,439 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_group -short_description: Manage AWS IAM groups -description: - - Manage AWS IAM groups. -version_added: "2.4" -author: -- Nick Aslanidis (@naslanidis) -- Maksym Postument (@infectsoldier) -options: - name: - description: - - The name of the group to create. - required: true - type: str - managed_policies: - description: - - A list of managed policy ARNs or friendly names to attach to the role. - - To embed an inline policy, use M(iam_policy). - required: false - type: list - elements: str - aliases: ['managed_policy'] - users: - description: - - A list of existing users to add as members of the group. - required: false - type: list - elements: str - state: - description: - - Create or remove the IAM group. - required: true - choices: [ 'present', 'absent' ] - type: str - purge_policies: - description: - - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. - required: false - default: false - type: bool - aliases: ['purge_policy', 'purge_managed_policies'] - purge_users: - description: - - When I(purge_users=true) users which are not included in I(users) will be detached. - required: false - default: false - type: bool -requirements: [ botocore, boto3 ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create a group -- iam_group: - name: testgroup1 - state: present - -# Create a group and attach a managed policy using its ARN -- iam_group: - name: testgroup1 - managed_policies: - - arn:aws:iam::aws:policy/AmazonSNSFullAccess - state: present - -# Create a group with users as members and attach a managed policy using its ARN -- iam_group: - name: testgroup1 - managed_policies: - - arn:aws:iam::aws:policy/AmazonSNSFullAccess - users: - - test_user1 - - test_user2 - state: present - -# Remove all managed policies from an existing group with an empty list -- iam_group: - name: testgroup1 - state: present - purge_policies: true - -# Remove all group members from an existing group -- iam_group: - name: testgroup1 - managed_policies: - - arn:aws:iam::aws:policy/AmazonSNSFullAccess - purge_users: true - state: present - - -# Delete the group -- iam_group: - name: testgroup1 - state: absent - -''' -RETURN = ''' -iam_group: - description: dictionary containing all the group information including group membership - returned: success - type: complex - contains: - group: - description: dictionary containing all the group information - returned: success - type: complex - contains: - arn: - description: the Amazon Resource Name (ARN) specifying the group - type: str - sample: "arn:aws:iam::1234567890:group/testgroup1" - create_date: - description: the date and time, in ISO 8601 date-time format, when the group was created - type: str - sample: "2017-02-08T04:36:28+00:00" - group_id: - description: the stable and unique string identifying the group - type: str - sample: AGPAIDBWE12NSFINE55TM - group_name: - description: the friendly name that identifies the group - type: str - sample: testgroup1 - path: - description: the path to the group - type: str - sample: / - users: - description: list containing all the group members - returned: success - type: complex - contains: - arn: - description: the Amazon Resource Name (ARN) specifying the user - type: str - sample: "arn:aws:iam::1234567890:user/test_user1" - create_date: - description: the date and time, in ISO 8601 date-time format, when the user was created - type: str - sample: "2017-02-08T04:36:28+00:00" - user_id: - description: the stable and unique string identifying the user - type: str - sample: AIDAIZTPY123YQRS22YU2 - user_name: - description: the friendly name that identifies the user - type: str - sample: testgroup1 - path: - description: the path to the user - type: str - sample: / -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.module_utils.ec2 import AWSRetry - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def compare_attached_group_policies(current_attached_policies, new_attached_policies): - - # If new_attached_policies is None it means we want to remove all policies - if len(current_attached_policies) > 0 and new_attached_policies is None: - return False - - current_attached_policies_arn_list = [] - for policy in current_attached_policies: - current_attached_policies_arn_list.append(policy['PolicyArn']) - - if set(current_attached_policies_arn_list) == set(new_attached_policies): - return True - else: - return False - - -def compare_group_members(current_group_members, new_group_members): - - # If new_attached_policies is None it means we want to remove all policies - if len(current_group_members) > 0 and new_group_members is None: - return False - if set(current_group_members) == set(new_group_members): - return True - else: - return False - - -def convert_friendly_names_to_arns(connection, module, policy_names): - - if not any([not policy.startswith('arn:') for policy in policy_names if policy is not None]): - return policy_names - allpolicies = {} - paginator = connection.get_paginator('list_policies') - policies = paginator.paginate().build_full_result()['Policies'] - - for policy in policies: - allpolicies[policy['PolicyName']] = policy['Arn'] - allpolicies[policy['Arn']] = policy['Arn'] - try: - return [allpolicies[policy] for policy in policy_names] - except KeyError as e: - module.fail_json(msg="Couldn't find policy: " + str(e)) - - -def create_or_update_group(connection, module): - - params = dict() - params['GroupName'] = module.params.get('name') - managed_policies = module.params.get('managed_policies') - users = module.params.get('users') - purge_users = module.params.get('purge_users') - purge_policies = module.params.get('purge_policies') - changed = False - if managed_policies: - managed_policies = convert_friendly_names_to_arns(connection, module, managed_policies) - - # Get group - try: - group = get_group(connection, module, params['GroupName']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't get group") - - # If group is None, create it - if group is None: - # Check mode means we would create the group - if module.check_mode: - module.exit_json(changed=True) - - try: - group = connection.create_group(**params) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't create group") - - # Manage managed policies - current_attached_policies = get_attached_policy_list(connection, module, params['GroupName']) - if not compare_attached_group_policies(current_attached_policies, managed_policies): - current_attached_policies_arn_list = [] - for policy in current_attached_policies: - current_attached_policies_arn_list.append(policy['PolicyArn']) - - # If managed_policies has a single empty element we want to remove all attached policies - if purge_policies: - # Detach policies not present - for policy_arn in list(set(current_attached_policies_arn_list) - set(managed_policies)): - changed = True - if not module.check_mode: - try: - connection.detach_group_policy(GroupName=params['GroupName'], PolicyArn=policy_arn) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't detach policy from group %s" % params['GroupName']) - # If there are policies to adjust that aren't in the current list, then things have changed - # Otherwise the only changes were in purging above - if set(managed_policies) - set(current_attached_policies_arn_list): - changed = True - # If there are policies in managed_policies attach each policy - if managed_policies != [None] and not module.check_mode: - for policy_arn in managed_policies: - try: - connection.attach_group_policy(GroupName=params['GroupName'], PolicyArn=policy_arn) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't attach policy to group %s" % params['GroupName']) - - # Manage group memberships - try: - current_group_members = get_group(connection, module, params['GroupName'])['Users'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't get group %s" % params['GroupName']) - - current_group_members_list = [] - for member in current_group_members: - current_group_members_list.append(member['UserName']) - - if not compare_group_members(current_group_members_list, users): - - if purge_users: - for user in list(set(current_group_members_list) - set(users)): - # Ensure we mark things have changed if any user gets purged - changed = True - # Skip actions for check mode - if not module.check_mode: - try: - connection.remove_user_from_group(GroupName=params['GroupName'], UserName=user) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't remove user %s from group %s" % (user, params['GroupName'])) - # If there are users to adjust that aren't in the current list, then things have changed - # Otherwise the only changes were in purging above - if set(users) - set(current_group_members_list): - changed = True - # Skip actions for check mode - if users != [None] and not module.check_mode: - for user in users: - try: - connection.add_user_to_group(GroupName=params['GroupName'], UserName=user) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't add user %s to group %s" % (user, params['GroupName'])) - if module.check_mode: - module.exit_json(changed=changed) - - # Get the group again - try: - group = get_group(connection, module, params['GroupName']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't get group %s" % params['GroupName']) - - module.exit_json(changed=changed, iam_group=camel_dict_to_snake_dict(group)) - - -def destroy_group(connection, module): - - params = dict() - params['GroupName'] = module.params.get('name') - - try: - group = get_group(connection, module, params['GroupName']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't get group %s" % params['GroupName']) - if group: - # Check mode means we would remove this group - if module.check_mode: - module.exit_json(changed=True) - - # Remove any attached policies otherwise deletion fails - try: - for policy in get_attached_policy_list(connection, module, params['GroupName']): - connection.detach_group_policy(GroupName=params['GroupName'], PolicyArn=policy['PolicyArn']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't remove policy from group %s" % params['GroupName']) - - # Remove any users in the group otherwise deletion fails - current_group_members_list = [] - try: - current_group_members = get_group(connection, module, params['GroupName'])['Users'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't get group %s" % params['GroupName']) - for member in current_group_members: - current_group_members_list.append(member['UserName']) - for user in current_group_members_list: - try: - connection.remove_user_from_group(GroupName=params['GroupName'], UserName=user) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't remove user %s from group %s" % (user, params['GroupName'])) - - try: - connection.delete_group(**params) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, "Couldn't delete group %s" % params['GroupName']) - - else: - module.exit_json(changed=False) - - module.exit_json(changed=True) - - -@AWSRetry.exponential_backoff() -def get_group(connection, module, name): - try: - paginator = connection.get_paginator('get_group') - return paginator.paginate(GroupName=name).build_full_result() - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return None - else: - raise - - -@AWSRetry.exponential_backoff() -def get_attached_policy_list(connection, module, name): - - try: - paginator = connection.get_paginator('list_attached_group_policies') - return paginator.paginate(GroupName=name).build_full_result()['AttachedPolicies'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return None - else: - raise - - -def main(): - - argument_spec = dict( - name=dict(required=True), - managed_policies=dict(default=[], type='list', aliases=['managed_policy']), - users=dict(default=[], type='list'), - state=dict(choices=['present', 'absent'], required=True), - purge_users=dict(default=False, type='bool'), - purge_policies=dict(default=False, type='bool', aliases=['purge_policy', 'purge_managed_policies']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - connection = module.client('iam') - - state = module.params.get("state") - - if state == 'present': - create_or_update_group(connection, module) - else: - destroy_group(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_managed_policy.py b/lib/ansible/modules/cloud/amazon/iam_managed_policy.py deleted file mode 100644 index e13c2bb6e1..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_managed_policy.py +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_managed_policy -short_description: Manage User Managed IAM policies -description: - - Allows creating and removing managed IAM policies -version_added: "2.4" -options: - policy_name: - description: - - The name of the managed policy. - required: True - type: str - policy_description: - description: - - A helpful description of this policy, this value is immutable and only set when creating a new policy. - default: '' - type: str - policy: - description: - - A properly json formatted policy - type: json - make_default: - description: - - Make this revision the default revision. - default: True - type: bool - only_version: - description: - - Remove all other non default revisions, if this is used with C(make_default) it will result in all other versions of this policy being deleted. - type: bool - default: false - state: - description: - - Should this managed policy be present or absent. Set to absent to detach all entities from this policy and remove it if found. - default: present - choices: [ "present", "absent" ] - type: str - fail_on_delete: - description: - - The I(fail_on_delete) option does nothing and will be removed in Ansible 2.14. - type: bool - -author: "Dan Kozlowski (@dkhenry)" -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore -''' - -EXAMPLES = ''' -# Create Policy ex nihilo -- name: Create IAM Managed Policy - iam_managed_policy: - policy_name: "ManagedPolicy" - policy_description: "A Helpful managed policy" - policy: "{{ lookup('template', 'managed_policy.json.j2') }}" - state: present - -# Update a policy with a new default version -- name: Create IAM Managed Policy - iam_managed_policy: - policy_name: "ManagedPolicy" - policy: "{{ lookup('file', 'managed_policy_update.json') }}" - state: present - -# Update a policy with a new non default version -- name: Create IAM Managed Policy - iam_managed_policy: - policy_name: "ManagedPolicy" - policy: "{{ lookup('file', 'managed_policy_update.json') }}" - make_default: false - state: present - -# Update a policy and make it the only version and the default version -- name: Create IAM Managed Policy - iam_managed_policy: - policy_name: "ManagedPolicy" - policy: "{ 'Version': '2012-10-17', 'Statement':[{'Effect': 'Allow','Action': '*','Resource': '*'}]}" - only_version: true - state: present - -# Remove a policy -- name: Create IAM Managed Policy - iam_managed_policy: - policy_name: "ManagedPolicy" - state: absent -''' - -RETURN = ''' -policy: - description: Returns the policy json structure, when state == absent this will return the value of the removed policy. - returned: success - type: str - sample: '{ - "arn": "arn:aws:iam::aws:policy/AdministratorAccess " - "attachment_count": 0, - "create_date": "2017-03-01T15:42:55.981000+00:00", - "default_version_id": "v1", - "is_attachable": true, - "path": "/", - "policy_id": "ANPALM4KLDMTFXGOOJIHL", - "policy_name": "AdministratorAccess", - "update_date": "2017-03-01T15:42:55.981000+00:00" - }' -''' - -import json -import traceback - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, get_aws_connection_info, ec2_argument_spec, AWSRetry, - camel_dict_to_snake_dict, HAS_BOTO3, compare_policies) -from ansible.module_utils._text import to_native - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_policies_with_backoff(iam): - paginator = iam.get_paginator('list_policies') - return paginator.paginate(Scope='Local').build_full_result() - - -def get_policy_by_name(module, iam, name): - try: - response = list_policies_with_backoff(iam) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't list policies: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for policy in response['Policies']: - if policy['PolicyName'] == name: - return policy - return None - - -def delete_oldest_non_default_version(module, iam, policy): - try: - versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])['Versions'] - if not v['IsDefaultVersion']] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't list policy versions: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - versions.sort(key=lambda v: v['CreateDate'], reverse=True) - for v in versions[-1:]: - try: - iam.delete_policy_version(PolicyArn=policy['Arn'], VersionId=v['VersionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't delete policy version: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - -# This needs to return policy_version, changed -def get_or_create_policy_version(module, iam, policy, policy_document): - try: - versions = iam.list_policy_versions(PolicyArn=policy['Arn'])['Versions'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't list policy versions: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for v in versions: - try: - document = iam.get_policy_version(PolicyArn=policy['Arn'], - VersionId=v['VersionId'])['PolicyVersion']['Document'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't get policy version %s: %s" % (v['VersionId'], str(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - # If the current policy matches the existing one - if not compare_policies(document, json.loads(to_native(policy_document))): - return v, False - - # No existing version so create one - # There is a service limit (typically 5) of policy versions. - # - # Rather than assume that it is 5, we'll try to create the policy - # and if that doesn't work, delete the oldest non default policy version - # and try again. - try: - version = iam.create_policy_version(PolicyArn=policy['Arn'], PolicyDocument=policy_document)['PolicyVersion'] - return version, True - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'LimitExceeded': - delete_oldest_non_default_version(module, iam, policy) - try: - version = iam.create_policy_version(PolicyArn=policy['Arn'], PolicyDocument=policy_document)['PolicyVersion'] - return version, True - except botocore.exceptions.ClientError as second_e: - e = second_e - # Handle both when the exception isn't LimitExceeded or - # the second attempt still failed - module.fail_json(msg="Couldn't create policy version: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - -def set_if_default(module, iam, policy, policy_version, is_default): - if is_default and not policy_version['IsDefaultVersion']: - try: - iam.set_default_policy_version(PolicyArn=policy['Arn'], VersionId=policy_version['VersionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't set default policy version: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - return True - return False - - -def set_if_only(module, iam, policy, policy_version, is_only): - if is_only: - try: - versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])[ - 'Versions'] if not v['IsDefaultVersion']] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't list policy versions: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for v in versions: - try: - iam.delete_policy_version(PolicyArn=policy['Arn'], VersionId=v['VersionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't delete policy version: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - return len(versions) > 0 - return False - - -def detach_all_entities(module, iam, policy, **kwargs): - try: - entities = iam.list_entities_for_policy(PolicyArn=policy['Arn'], **kwargs) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't detach list entities for policy %s: %s" % (policy['PolicyName'], str(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - for g in entities['PolicyGroups']: - try: - iam.detach_group_policy(PolicyArn=policy['Arn'], GroupName=g['GroupName']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't detach group policy %s: %s" % (g['GroupName'], str(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for u in entities['PolicyUsers']: - try: - iam.detach_user_policy(PolicyArn=policy['Arn'], UserName=u['UserName']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't detach user policy %s: %s" % (u['UserName'], str(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for r in entities['PolicyRoles']: - try: - iam.detach_role_policy(PolicyArn=policy['Arn'], RoleName=r['RoleName']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't detach role policy %s: %s" % (r['RoleName'], str(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - if entities['IsTruncated']: - detach_all_entities(module, iam, policy, marker=entities['Marker']) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - policy_name=dict(required=True), - policy_description=dict(default=''), - policy=dict(type='json'), - make_default=dict(type='bool', default=True), - only_version=dict(type='bool', default=False), - fail_on_delete=dict(type='bool', removed_in_version='2.14'), - state=dict(default='present', choices=['present', 'absent']), - )) - - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[['state', 'present', ['policy']]] - ) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for this module') - - name = module.params.get('policy_name') - description = module.params.get('policy_description') - state = module.params.get('state') - default = module.params.get('make_default') - only = module.params.get('only_version') - - policy = None - - if module.params.get('policy') is not None: - policy = json.dumps(json.loads(module.params.get('policy'))) - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - iam = boto3_conn(module, conn_type='client', resource='iam', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - except (botocore.exceptions.NoCredentialsError, botocore.exceptions.ProfileNotFound) as e: - module.fail_json(msg="Can't authorize connection. Check your credentials and profile.", - exceptions=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - p = get_policy_by_name(module, iam, name) - if state == 'present': - if p is None: - # No Policy so just create one - try: - rvalue = iam.create_policy(PolicyName=name, Path='/', - PolicyDocument=policy, Description=description) - except Exception as e: - module.fail_json(msg="Couldn't create policy %s: %s" % (name, to_native(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - module.exit_json(changed=True, policy=camel_dict_to_snake_dict(rvalue['Policy'])) - else: - policy_version, changed = get_or_create_policy_version(module, iam, p, policy) - changed = set_if_default(module, iam, p, policy_version, default) or changed - changed = set_if_only(module, iam, p, policy_version, only) or changed - # If anything has changed we needto refresh the policy - if changed: - try: - p = iam.get_policy(PolicyArn=p['Arn'])['Policy'] - except Exception as e: - module.fail_json(msg="Couldn't get policy: %s" % to_native(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - module.exit_json(changed=changed, policy=camel_dict_to_snake_dict(p)) - else: - # Check for existing policy - if p: - # Detach policy - detach_all_entities(module, iam, p) - # Delete Versions - try: - versions = iam.list_policy_versions(PolicyArn=p['Arn'])['Versions'] - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't list policy versions: %s" % to_native(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - for v in versions: - if not v['IsDefaultVersion']: - try: - iam.delete_policy_version(PolicyArn=p['Arn'], VersionId=v['VersionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't delete policy version %s: %s" % - (v['VersionId'], to_native(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - # Delete policy - try: - iam.delete_policy(PolicyArn=p['Arn']) - except Exception as e: - module.fail_json(msg="Couldn't delete policy %s: %s" % (p['PolicyName'], to_native(e)), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - # This is the one case where we will return the old policy - module.exit_json(changed=True, policy=camel_dict_to_snake_dict(p)) - else: - module.exit_json(changed=False, policy=None) -# end main - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_mfa_device_info.py b/lib/ansible/modules/cloud/amazon/iam_mfa_device_info.py deleted file mode 100644 index b09da4da5e..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_mfa_device_info.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_mfa_device_info -short_description: List the MFA (Multi-Factor Authentication) devices registered for a user -description: - - List the MFA (Multi-Factor Authentication) devices registered for a user - - This module was called C(iam_mfa_device_facts) before Ansible 2.9. The usage did not change. -version_added: "2.2" -author: Victor Costan (@pwnall) -options: - user_name: - description: - - The name of the user whose MFA devices will be listed - type: str -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore -''' - -RETURN = """ -mfa_devices: - description: The MFA devices registered for the given user - returned: always - type: list - sample: - - enable_date: "2016-03-11T23:25:36+00:00" - serial_number: arn:aws:iam::085120003701:mfa/pwnall - user_name: pwnall - - enable_date: "2016-03-11T23:25:37+00:00" - serial_number: arn:aws:iam::085120003702:mfa/pwnall - user_name: pwnall -""" - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# List MFA devices (more details: https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListMFADevices.html) -- iam_mfa_device_info: - register: mfa_devices - -# Assume an existing role (more details: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) -- sts_assume_role: - mfa_serial_number: "{{ mfa_devices.mfa_devices[0].serial_number }}" - role_arn: "arn:aws:iam::123456789012:role/someRole" - role_session_name: "someRoleSession" - register: assumed_role -''' - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info) - - -def list_mfa_devices(connection, module): - user_name = module.params.get('user_name') - changed = False - - args = {} - if user_name is not None: - args['UserName'] = user_name - try: - response = connection.list_mfa_devices(**args) - except ClientError as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - user_name=dict(required=False, default=None) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - if module._name == 'iam_mfa_device_facts': - module.deprecate("The 'iam_mfa_device_facts' module has been renamed to 'iam_mfa_device_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if region: - connection = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) - else: - module.fail_json(msg="region must be specified") - - list_mfa_devices(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_password_policy.py b/lib/ansible/modules/cloud/amazon/iam_password_policy.py deleted file mode 100644 index 08334992fc..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_password_policy.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_password_policy -short_description: Update an IAM Password Policy -description: - - Module updates an IAM Password Policy on a given AWS account -version_added: "2.8" -requirements: [ 'botocore', 'boto3' ] -author: - - "Aaron Smith (@slapula)" -options: - state: - description: - - Specifies the overall state of the password policy. - required: true - choices: ['present', 'absent'] - type: str - min_pw_length: - description: - - Minimum password length. - default: 6 - aliases: [minimum_password_length] - type: int - require_symbols: - description: - - Require symbols in password. - default: false - type: bool - require_numbers: - description: - - Require numbers in password. - default: false - type: bool - require_uppercase: - description: - - Require uppercase letters in password. - default: false - type: bool - require_lowercase: - description: - - Require lowercase letters in password. - default: false - type: bool - allow_pw_change: - description: - - Allow users to change their password. - default: false - type: bool - aliases: [allow_password_change] - pw_max_age: - description: - - Maximum age for a password in days. When this option is 0 then passwords - do not expire automatically. - default: 0 - aliases: [password_max_age] - type: int - pw_reuse_prevent: - description: - - Prevent re-use of passwords. - default: 0 - aliases: [password_reuse_prevent, prevent_reuse] - type: int - pw_expire: - description: - - Prevents users from change an expired password. - default: false - type: bool - aliases: [password_expire, expire] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Password policy for AWS account - iam_password_policy: - state: present - min_pw_length: 8 - require_symbols: false - require_numbers: true - require_uppercase: true - require_lowercase: true - allow_pw_change: true - pw_max_age: 60 - pw_reuse_prevent: 5 - pw_expire: false -''' - -RETURN = ''' # ''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -class IAMConnection(object): - def __init__(self, module): - try: - self.connection = module.resource('iam') - self.module = module - except Exception as e: - module.fail_json(msg="Failed to connect to AWS: %s" % str(e)) - - def policy_to_dict(self, policy): - policy_attributes = [ - 'allow_users_to_change_password', 'expire_passwords', 'hard_expiry', - 'max_password_age', 'minimum_password_length', 'password_reuse_prevention', - 'require_lowercase_characters', 'require_numbers', 'require_symbols', 'require_uppercase_characters' - ] - ret = {} - for attr in policy_attributes: - ret[attr] = getattr(policy, attr) - return ret - - def update_password_policy(self, module, policy): - min_pw_length = module.params.get('min_pw_length') - require_symbols = module.params.get('require_symbols') - require_numbers = module.params.get('require_numbers') - require_uppercase = module.params.get('require_uppercase') - require_lowercase = module.params.get('require_lowercase') - allow_pw_change = module.params.get('allow_pw_change') - pw_max_age = module.params.get('pw_max_age') - pw_reuse_prevent = module.params.get('pw_reuse_prevent') - pw_expire = module.params.get('pw_expire') - - update_parameters = dict( - MinimumPasswordLength=min_pw_length, - RequireSymbols=require_symbols, - RequireNumbers=require_numbers, - RequireUppercaseCharacters=require_uppercase, - RequireLowercaseCharacters=require_lowercase, - AllowUsersToChangePassword=allow_pw_change, - HardExpiry=pw_expire - ) - if pw_reuse_prevent: - update_parameters.update(PasswordReusePrevention=pw_reuse_prevent) - if pw_max_age: - update_parameters.update(MaxPasswordAge=pw_max_age) - - try: - original_policy = self.policy_to_dict(policy) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - original_policy = {} - - try: - results = policy.update(**update_parameters) - policy.reload() - updated_policy = self.policy_to_dict(policy) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't update IAM Password Policy") - - changed = (original_policy != updated_policy) - return (changed, updated_policy, camel_dict_to_snake_dict(results)) - - def delete_password_policy(self, policy): - try: - results = policy.delete() - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - self.module.exit_json(changed=False, task_status={'IAM': "Couldn't find IAM Password Policy"}) - else: - self.module.fail_json_aws(e, msg="Couldn't delete IAM Password Policy") - return camel_dict_to_snake_dict(results) - - -def main(): - module = AnsibleAWSModule( - argument_spec={ - 'state': dict(choices=['present', 'absent'], required=True), - 'min_pw_length': dict(type='int', aliases=['minimum_password_length'], default=6), - 'require_symbols': dict(type='bool', default=False), - 'require_numbers': dict(type='bool', default=False), - 'require_uppercase': dict(type='bool', default=False), - 'require_lowercase': dict(type='bool', default=False), - 'allow_pw_change': dict(type='bool', aliases=['allow_password_change'], default=False), - 'pw_max_age': dict(type='int', aliases=['password_max_age'], default=0), - 'pw_reuse_prevent': dict(type='int', aliases=['password_reuse_prevent', 'prevent_reuse'], default=0), - 'pw_expire': dict(type='bool', aliases=['password_expire', 'expire'], default=False), - }, - supports_check_mode=True, - ) - - resource = IAMConnection(module) - policy = resource.connection.AccountPasswordPolicy() - - state = module.params.get('state') - - if state == 'present': - (changed, new_policy, update_result) = resource.update_password_policy(module, policy) - module.exit_json(changed=changed, task_status={'IAM': update_result}, policy=new_policy) - - if state == 'absent': - delete_result = resource.delete_password_policy(policy) - module.exit_json(changed=True, task_status={'IAM': delete_result}) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_policy.py b/lib/ansible/modules/cloud/amazon/iam_policy.py deleted file mode 100644 index 8db58e083a..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_policy.py +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_policy -short_description: Manage inline IAM policies for users, groups, and roles -description: - - Allows uploading or removing inline IAM policies for IAM users, groups or roles. - - To administer managed policies please see M(iam_user), M(iam_role), - M(iam_group) and M(iam_managed_policy) -version_added: "2.0" -options: - iam_type: - description: - - Type of IAM resource. - required: true - choices: [ "user", "group", "role"] - type: str - iam_name: - description: - - Name of IAM resource you wish to target for policy actions. In other words, the user name, group name or role name. - required: true - type: str - policy_name: - description: - - The name label for the policy to create or remove. - required: true - type: str - policy_document: - description: - - The path to the properly json formatted policy file. - - Mutually exclusive with I(policy_json). - - This option has been deprecated and will be removed in 2.14. The existing behavior can be - reproduced by using the I(policy_json) option and reading the file using the lookup plugin. - type: str - policy_json: - description: - - A properly json formatted policy as string. - - Mutually exclusive with I(policy_document). - - See U(https://github.com/ansible/ansible/issues/7005#issuecomment-42894813) on how to use it properly. - type: json - state: - description: - - Whether to create or delete the IAM policy. - choices: [ "present", "absent"] - default: present - type: str - skip_duplicates: - description: - - When I(skip_duplicates=true) the module looks for any policies that match the document you pass in. If there is a match it will not make - a new policy object with the same rules. - - The current default is C(true). However, this behavior can be confusing and as such the default will change to C(false) in 2.14. To maintain - the existing behavior explicitly set I(skip_duplicates=true). - type: bool - -author: - - "Jonathan I. Davila (@defionscode)" - - "Dennis Podkovyrin (@sbj-ss)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create a policy with the name of 'Admin' to the group 'administrators' -- name: Assign a policy called Admin to the administrators group - iam_policy: - iam_type: group - iam_name: administrators - policy_name: Admin - state: present - policy_document: admin_policy.json - -# Advanced example, create two new groups and add a READ-ONLY policy to both -# groups. -- name: Create Two Groups, Mario and Luigi - iam: - iam_type: group - name: "{{ item }}" - state: present - loop: - - Mario - - Luigi - register: new_groups - -- name: Apply READ-ONLY policy to new groups that have been recently created - iam_policy: - iam_type: group - iam_name: "{{ item.created_group.group_name }}" - policy_name: "READ-ONLY" - policy_document: readonlypolicy.json - state: present - loop: "{{ new_groups.results }}" - -# Create a new S3 policy with prefix per user -- name: Create S3 policy from template - iam_policy: - iam_type: user - iam_name: "{{ item.user }}" - policy_name: "s3_limited_access_{{ item.prefix }}" - state: present - policy_json: " {{ lookup( 'template', 's3_policy.json.j2') }} " - loop: - - user: s3_user - prefix: s3_user_prefix - -''' -import json - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import compare_policies -from ansible.module_utils.six import string_types - - -class PolicyError(Exception): - pass - - -class Policy: - - def __init__(self, client, name, policy_name, policy_document, policy_json, skip_duplicates, state, check_mode): - self.client = client - self.name = name - self.policy_name = policy_name - self.policy_document = policy_document - self.policy_json = policy_json - self.skip_duplicates = skip_duplicates - self.state = state - self.check_mode = check_mode - self.changed = False - - @staticmethod - def _iam_type(): - return '' - - def _list(self, name): - return {} - - def list(self): - return self._list(self.name).get('PolicyNames', []) - - def _get(self, name, policy_name): - return '{}' - - def get(self, policy_name): - return self._get(self.name, policy_name)['PolicyDocument'] - - def _put(self, name, policy_name, policy_doc): - pass - - def put(self, policy_doc): - if not self.check_mode: - self._put(self.name, self.policy_name, json.dumps(policy_doc, sort_keys=True)) - self.changed = True - - def _delete(self, name, policy_name): - pass - - def delete(self): - if self.policy_name not in self.list(): - self.changed = False - return - - self.changed = True - if not self.check_mode: - self._delete(self.name, self.policy_name) - - def get_policy_text(self): - try: - if self.policy_document is not None: - return self.get_policy_from_document() - if self.policy_json is not None: - return self.get_policy_from_json() - except json.JSONDecodeError as e: - raise PolicyError('Failed to decode the policy as valid JSON: %s' % str(e)) - return None - - def get_policy_from_document(self): - try: - with open(self.policy_document, 'r') as json_data: - pdoc = json.load(json_data) - json_data.close() - except IOError as e: - if e.errno == 2: - raise PolicyError('policy_document {0:!r} does not exist'.format(self.policy_document)) - raise - return pdoc - - def get_policy_from_json(self): - if isinstance(self.policy_json, string_types): - pdoc = json.loads(self.policy_json) - else: - pdoc = self.policy_json - return pdoc - - def create(self): - matching_policies = [] - policy_doc = self.get_policy_text() - policy_match = False - for pol in self.list(): - if not compare_policies(self.get(pol), policy_doc): - matching_policies.append(pol) - policy_match = True - - if (self.policy_name not in matching_policies) and not (self.skip_duplicates and policy_match): - self.put(policy_doc) - - def run(self): - if self.state == 'present': - self.create() - elif self.state == 'absent': - self.delete() - return { - 'changed': self.changed, - self._iam_type() + '_name': self.name, - 'policies': self.list() - } - - -class UserPolicy(Policy): - - @staticmethod - def _iam_type(): - return 'user' - - def _list(self, name): - return self.client.list_user_policies(UserName=name) - - def _get(self, name, policy_name): - return self.client.get_user_policy(UserName=name, PolicyName=policy_name) - - def _put(self, name, policy_name, policy_doc): - return self.client.put_user_policy(UserName=name, PolicyName=policy_name, PolicyDocument=policy_doc) - - def _delete(self, name, policy_name): - return self.client.delete_user_policy(UserName=name, PolicyName=policy_name) - - -class RolePolicy(Policy): - - @staticmethod - def _iam_type(): - return 'role' - - def _list(self, name): - return self.client.list_role_policies(RoleName=name) - - def _get(self, name, policy_name): - return self.client.get_role_policy(RoleName=name, PolicyName=policy_name) - - def _put(self, name, policy_name, policy_doc): - return self.client.put_role_policy(RoleName=name, PolicyName=policy_name, PolicyDocument=policy_doc) - - def _delete(self, name, policy_name): - return self.client.delete_role_policy(RoleName=name, PolicyName=policy_name) - - -class GroupPolicy(Policy): - - @staticmethod - def _iam_type(): - return 'group' - - def _list(self, name): - return self.client.list_group_policies(GroupName=name) - - def _get(self, name, policy_name): - return self.client.get_group_policy(GroupName=name, PolicyName=policy_name) - - def _put(self, name, policy_name, policy_doc): - return self.client.put_group_policy(GroupName=name, PolicyName=policy_name, PolicyDocument=policy_doc) - - def _delete(self, name, policy_name): - return self.client.delete_group_policy(GroupName=name, PolicyName=policy_name) - - -def main(): - argument_spec = dict( - iam_type=dict(required=True, choices=['user', 'group', 'role']), - state=dict(default='present', choices=['present', 'absent']), - iam_name=dict(required=True), - policy_name=dict(required=True), - policy_document=dict(default=None, required=False), - policy_json=dict(type='json', default=None, required=False), - skip_duplicates=dict(type='bool', default=None, required=False) - ) - mutually_exclusive = [['policy_document', 'policy_json']] - - module = AnsibleAWSModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True) - - skip_duplicates = module.params.get('skip_duplicates') - - if (skip_duplicates is None): - module.deprecate('The skip_duplicates behaviour has caused confusion and' - ' will be disabled by default in Ansible 2.14', - version='2.14') - skip_duplicates = True - - if module.params.get('policy_document'): - module.deprecate('The policy_document option has been deprecated and' - ' will be removed in Ansible 2.14', - version='2.14') - - args = dict( - client=module.client('iam'), - name=module.params.get('iam_name'), - policy_name=module.params.get('policy_name'), - policy_document=module.params.get('policy_document'), - policy_json=module.params.get('policy_json'), - skip_duplicates=skip_duplicates, - state=module.params.get('state'), - check_mode=module.check_mode, - ) - iam_type = module.params.get('iam_type') - - try: - if iam_type == 'user': - policy = UserPolicy(**args) - elif iam_type == 'role': - policy = RolePolicy(**args) - elif iam_type == 'group': - policy = GroupPolicy(**args) - - module.exit_json(**(policy.run())) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - except PolicyError as e: - module.fail_json(msg=str(e)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_policy_info.py b/lib/ansible/modules/cloud/amazon/iam_policy_info.py deleted file mode 100644 index ba2fc69400..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_policy_info.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_policy_info -short_description: Retrieve inline IAM policies for users, groups, and roles -description: - - Supports fetching of inline IAM policies for IAM users, groups and roles. -version_added: "2.10" -options: - iam_type: - description: - - Type of IAM resource you wish to retrieve inline policies for. - required: yes - choices: [ "user", "group", "role"] - type: str - iam_name: - description: - - Name of IAM resource you wish to retrieve inline policies for. In other words, the user name, group name or role name. - required: yes - type: str - policy_name: - description: - - Name of a specific IAM inline policy you with to retrieve. - required: no - type: str - -author: - - Mark Chappell (@tremble) - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Describe all inline IAM policies on an IAM User -- iam_policy_info: - iam_type: user - iam_name: example_user - -# Describe a specific inline policy on an IAM Role -- iam_policy_info: - iam_type: role - iam_name: example_role - policy_name: example_policy - -''' -RETURN = ''' -policies: - description: A list containing the matching IAM inline policy names and their data - returned: success - type: complex - contains: - policy_name: - description: The Name of the inline policy - returned: success - type: str - policy_document: - description: The JSON document representing the inline IAM policy - returned: success - type: list -policy_names: - description: A list of matching names of the IAM inline policies on the queried object - returned: success - type: list -all_policy_names: - description: A list of names of all of the IAM inline policies on the queried object - returned: success - type: list -''' - -import json - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.six import string_types - - -class PolicyError(Exception): - pass - - -class Policy: - - def __init__(self, client, name, policy_name): - self.client = client - self.name = name - self.policy_name = policy_name - self.changed = False - - @staticmethod - def _iam_type(): - return '' - - def _list(self, name): - return {} - - def list(self): - return self._list(self.name).get('PolicyNames', []) - - def _get(self, name, policy_name): - return '{}' - - def get(self, policy_name): - return self._get(self.name, policy_name)['PolicyDocument'] - - def get_all(self): - policies = list() - for policy in self.list(): - policies.append({"policy_name": policy, "policy_document": self.get(policy)}) - return policies - - def run(self): - policy_list = self.list() - ret_val = { - 'changed': False, - self._iam_type() + '_name': self.name, - 'all_policy_names': policy_list - } - if self.policy_name is None: - ret_val.update(policies=self.get_all()) - ret_val.update(policy_names=policy_list) - elif self.policy_name in policy_list: - ret_val.update(policies=[{ - "policy_name": self.policy_name, - "policy_document": self.get(self.policy_name)}]) - ret_val.update(policy_names=[self.policy_name]) - return ret_val - - -class UserPolicy(Policy): - - @staticmethod - def _iam_type(): - return 'user' - - def _list(self, name): - return self.client.list_user_policies(UserName=name) - - def _get(self, name, policy_name): - return self.client.get_user_policy(UserName=name, PolicyName=policy_name) - - -class RolePolicy(Policy): - - @staticmethod - def _iam_type(): - return 'role' - - def _list(self, name): - return self.client.list_role_policies(RoleName=name) - - def _get(self, name, policy_name): - return self.client.get_role_policy(RoleName=name, PolicyName=policy_name) - - -class GroupPolicy(Policy): - - @staticmethod - def _iam_type(): - return 'group' - - def _list(self, name): - return self.client.list_group_policies(GroupName=name) - - def _get(self, name, policy_name): - return self.client.get_group_policy(GroupName=name, PolicyName=policy_name) - - -def main(): - argument_spec = dict( - iam_type=dict(required=True, choices=['user', 'group', 'role']), - iam_name=dict(required=True), - policy_name=dict(default=None, required=False), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - args = dict( - client=module.client('iam'), - name=module.params.get('iam_name'), - policy_name=module.params.get('policy_name'), - ) - iam_type = module.params.get('iam_type') - - try: - if iam_type == 'user': - policy = UserPolicy(**args) - elif iam_type == 'role': - policy = RolePolicy(**args) - elif iam_type == 'group': - policy = GroupPolicy(**args) - - module.exit_json(**(policy.run())) - except (BotoCoreError, ClientError) as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - module.exit_json(changed=False, msg=e.response['Error']['Message']) - module.fail_json_aws(e) - except PolicyError as e: - module.fail_json(msg=str(e)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_role.py b/lib/ansible/modules/cloud/amazon/iam_role.py deleted file mode 100644 index 71a5b0377e..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_role.py +++ /dev/null @@ -1,673 +0,0 @@ -#!/usr/bin/python -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_role -short_description: Manage AWS IAM roles -description: - - Manage AWS IAM roles. -version_added: "2.3" -author: "Rob White (@wimnat)" -options: - path: - description: - - The path to the role. For more information about paths, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). - default: "/" - type: str - name: - description: - - The name of the role to create. - required: true - type: str - description: - description: - - Provides a description of the role. - version_added: "2.5" - type: str - boundary: - description: - - The ARN of an IAM managed policy to use to restrict the permissions this role can pass on to IAM roles/users that it creates. - - Boundaries cannot be set on Instance Profiles, as such if this option is specified then I(create_instance_profile) must be C(false). - - This is intended for roles/users that have permissions to create new IAM objects. - - For more information on boundaries, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). - - Requires botocore 1.10.57 or above. - aliases: [boundary_policy_arn] - version_added: "2.7" - type: str - assume_role_policy_document: - description: - - The trust relationship policy document that grants an entity permission to assume the role. - - This parameter is required when I(state=present). - type: json - managed_policies: - description: - - A list of managed policy ARNs or, since Ansible 2.4, a list of either managed policy ARNs or friendly names. - - To remove all policies set I(purge_polices=true) and I(managed_policies=[None]). - - To embed an inline policy, use M(iam_policy). - aliases: ['managed_policy'] - type: list - max_session_duration: - description: - - The maximum duration (in seconds) of a session when assuming the role. - - Valid values are between 1 and 12 hours (3600 and 43200 seconds). - version_added: "2.10" - type: int - purge_policies: - description: - - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. - - By default I(purge_policies=true). In Ansible 2.14 this will be changed to I(purge_policies=false). - version_added: "2.5" - type: bool - aliases: ['purge_policy', 'purge_managed_policies'] - state: - description: - - Create or remove the IAM role. - default: present - choices: [ present, absent ] - type: str - create_instance_profile: - description: - - Creates an IAM instance profile along with the role. - default: true - version_added: "2.5" - type: bool - delete_instance_profile: - description: - - When I(delete_instance_profile=true) and I(state=absent) deleting a role will also delete the instance - profile created with the same I(name) as the role. - - Only applies when I(state=absent). - default: false - version_added: "2.10" - type: bool - tags: - description: - - Tag dict to apply to the queue. - - Requires botocore 1.12.46 or above. - version_added: "2.10" - type: dict - purge_tags: - description: - - Remove tags not listed in I(tags) when tags is specified. - default: true - version_added: "2.10" - type: bool -requirements: [ botocore, boto3 ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Create a role with description and tags - iam_role: - name: mynewrole - assume_role_policy_document: "{{ lookup('file','policy.json') }}" - description: This is My New Role - tags: - env: dev - -- name: "Create a role and attach a managed policy called 'PowerUserAccess'" - iam_role: - name: mynewrole - assume_role_policy_document: "{{ lookup('file','policy.json') }}" - managed_policies: - - arn:aws:iam::aws:policy/PowerUserAccess - -- name: Keep the role created above but remove all managed policies - iam_role: - name: mynewrole - assume_role_policy_document: "{{ lookup('file','policy.json') }}" - managed_policies: [] - -- name: Delete the role - iam_role: - name: mynewrole - assume_role_policy_document: "{{ lookup('file', 'policy.json') }}" - state: absent - -''' -RETURN = ''' -iam_role: - description: dictionary containing the IAM Role data - returned: success - type: complex - contains: - path: - description: the path to the role - type: str - returned: always - sample: / - role_name: - description: the friendly name that identifies the role - type: str - returned: always - sample: myrole - role_id: - description: the stable and unique string identifying the role - type: str - returned: always - sample: ABCDEFF4EZ4ABCDEFV4ZC - arn: - description: the Amazon Resource Name (ARN) specifying the role - type: str - returned: always - sample: "arn:aws:iam::1234567890:role/mynewrole" - create_date: - description: the date and time, in ISO 8601 date-time format, when the role was created - type: str - returned: always - sample: "2016-08-14T04:36:28+00:00" - assume_role_policy_document: - description: the policy that grants an entity permission to assume the role - type: str - returned: always - sample: { - 'statement': [ - { - 'action': 'sts:AssumeRole', - 'effect': 'Allow', - 'principal': { - 'service': 'ec2.amazonaws.com' - }, - 'sid': '' - } - ], - 'version': '2012-10-17' - } - attached_policies: - description: a list of dicts containing the name and ARN of the managed IAM policies attached to the role - type: list - returned: always - sample: [ - { - 'policy_arn': 'arn:aws:iam::aws:policy/PowerUserAccess', - 'policy_name': 'PowerUserAccess' - } - ] - tags: - description: role tags - type: dict - returned: always - sample: '{"Env": "Prod"}' -''' - -import json - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, compare_policies -from ansible.module_utils.ec2 import AWSRetry, ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict, compare_aws_tags - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - - -def compare_assume_role_policy_doc(current_policy_doc, new_policy_doc): - if not compare_policies(current_policy_doc, json.loads(new_policy_doc)): - return True - else: - return False - - -@AWSRetry.jittered_backoff() -def _list_policies(connection): - paginator = connection.get_paginator('list_policies') - return paginator.paginate().build_full_result()['Policies'] - - -def convert_friendly_names_to_arns(connection, module, policy_names): - if not any([not policy.startswith('arn:') for policy in policy_names]): - return policy_names - allpolicies = {} - policies = _list_policies(connection) - - for policy in policies: - allpolicies[policy['PolicyName']] = policy['Arn'] - allpolicies[policy['Arn']] = policy['Arn'] - try: - return [allpolicies[policy] for policy in policy_names] - except KeyError as e: - module.fail_json_aws(e, msg="Couldn't find policy") - - -def attach_policies(connection, module, policies_to_attach, params): - changed = False - for policy_arn in policies_to_attach: - try: - if not module.check_mode: - connection.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) - changed = True - return changed - - -def remove_policies(connection, module, policies_to_remove, params): - changed = False - for policy in policies_to_remove: - try: - if not module.check_mode: - connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) - changed = True - return changed - - -def generate_create_params(module): - params = dict() - params['Path'] = module.params.get('path') - params['RoleName'] = module.params.get('name') - params['AssumeRolePolicyDocument'] = module.params.get('assume_role_policy_document') - if module.params.get('description') is not None: - params['Description'] = module.params.get('description') - if module.params.get('max_session_duration') is not None: - params['MaxSessionDuration'] = module.params.get('max_session_duration') - if module.params.get('boundary') is not None: - params['PermissionsBoundary'] = module.params.get('boundary') - if module.params.get('tags') is not None: - params['Tags'] = ansible_dict_to_boto3_tag_list(module.params.get('tags')) - - return params - - -def create_basic_role(connection, module, params): - """ - Perform the Role creation. - Assumes tests for the role existing have already been performed. - """ - - try: - if not module.check_mode: - role = connection.create_role(aws_retry=True, **params) - # 'Description' is documented as key of the role returned by create_role - # but appears to be an AWS bug (the value is not returned using the AWS CLI either). - # Get the role after creating it. - role = get_role_with_backoff(connection, module, params['RoleName']) - else: - role = {'MadeInCheckMode': True} - role['AssumeRolePolicyDocument'] = json.loads(params['AssumeRolePolicyDocument']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to create role") - - return role - - -def update_role_assumed_policy(connection, module, params, role): - # Check Assumed Policy document - if compare_assume_role_policy_doc(role['AssumeRolePolicyDocument'], params['AssumeRolePolicyDocument']): - return False - - if module.check_mode: - return True - - try: - connection.update_assume_role_policy( - RoleName=params['RoleName'], - PolicyDocument=json.dumps(json.loads(params['AssumeRolePolicyDocument'])), - aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) - return True - - -def update_role_description(connection, module, params, role): - # Check Description update - if params.get('Description') is None: - return False - if role.get('Description') == params['Description']: - return False - - if module.check_mode: - return True - - try: - connection.update_role_description(RoleName=params['RoleName'], Description=params['Description'], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update description for role {0}".format(params['RoleName'])) - return True - - -def update_role_max_session_duration(connection, module, params, role): - # Check MaxSessionDuration update - if params.get('MaxSessionDuration') is None: - return False - if role.get('MaxSessionDuration') == params['MaxSessionDuration']: - return False - - if module.check_mode: - return True - - try: - connection.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration'], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update maximum session duration for role {0}".format(params['RoleName'])) - return True - - -def update_role_permissions_boundary(connection, module, params, role): - # Check PermissionsBoundary - if params.get('PermissionsBoundary') is None: - return False - if params.get('PermissionsBoundary') == role.get('PermissionsBoundary', {}).get('PermissionsBoundaryArn', ''): - return False - - if module.check_mode: - return True - - if params.get('PermissionsBoundary') == '': - try: - connection.delete_role_permissions_boundary(RoleName=params['RoleName'], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}".format(params['RoleName'])) - else: - try: - connection.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary'], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}".format(params['RoleName'])) - return True - - -def update_managed_policies(connection, module, params, role, managed_policies, purge_policies): - # Check Managed Policies - if managed_policies is None: - return False - - # If we're manipulating a fake role - if role.get('MadeInCheckMode', False): - role['AttachedPolicies'] = list(map(lambda x: {'PolicyArn': x, 'PolicyName': x.split(':')[5]}, managed_policies)) - return True - - # Get list of current attached managed policies - current_attached_policies = get_attached_policy_list(connection, module, params['RoleName']) - current_attached_policies_arn_list = [policy['PolicyArn'] for policy in current_attached_policies] - - if len(managed_policies) == 1 and managed_policies[0] is None: - managed_policies = [] - - policies_to_remove = set(current_attached_policies_arn_list) - set(managed_policies) - policies_to_attach = set(managed_policies) - set(current_attached_policies_arn_list) - - changed = False - - if purge_policies: - changed |= remove_policies(connection, module, policies_to_remove, params) - - changed |= attach_policies(connection, module, policies_to_attach, params) - - return changed - - -def create_or_update_role(connection, module): - - params = generate_create_params(module) - role_name = params['RoleName'] - create_instance_profile = module.params.get('create_instance_profile') - purge_policies = module.params.get('purge_policies') - if purge_policies is None: - purge_policies = True - managed_policies = module.params.get('managed_policies') - if managed_policies: - # Attempt to list the policies early so we don't leave things behind if we can't find them. - managed_policies = convert_friendly_names_to_arns(connection, module, managed_policies) - - changed = False - - # Get role - role = get_role(connection, module, role_name) - - # If role is None, create it - if role is None: - role = create_basic_role(connection, module, params) - changed = True - else: - changed |= update_role_tags(connection, module, params, role) - changed |= update_role_assumed_policy(connection, module, params, role) - changed |= update_role_description(connection, module, params, role) - changed |= update_role_max_session_duration(connection, module, params, role) - changed |= update_role_permissions_boundary(connection, module, params, role) - - if create_instance_profile: - changed |= create_instance_profiles(connection, module, params, role) - - changed |= update_managed_policies(connection, module, params, role, managed_policies, purge_policies) - - # Get the role again - if not role.get('MadeInCheckMode', False): - role = get_role(connection, module, params['RoleName']) - role['AttachedPolicies'] = get_attached_policy_list(connection, module, params['RoleName']) - role['tags'] = get_role_tags(connection, module) - - module.exit_json( - changed=changed, iam_role=camel_dict_to_snake_dict(role, ignore_list=['tags']), - **camel_dict_to_snake_dict(role, ignore_list=['tags'])) - - -def create_instance_profiles(connection, module, params, role): - - if role.get('MadeInCheckMode', False): - return False - - # Fetch existing Profiles - try: - instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'], aws_retry=True)['InstanceProfiles'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - - # Profile already exists - if any(p['InstanceProfileName'] == params['RoleName'] for p in instance_profiles): - return False - - if module.check_mode: - return True - - # Make sure an instance profile is created - try: - connection.create_instance_profile(InstanceProfileName=params['RoleName'], Path=params['Path'], aws_retry=True) - except ClientError as e: - # If the profile already exists, no problem, move on. - # Implies someone's changing things at the same time... - if e.response['Error']['Code'] == 'EntityAlreadyExists': - return False - else: - module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) - - # And attach the role to the profile - try: - connection.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName'], aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to attach role {0} to instance profile {0}".format(params['RoleName'])) - - return True - - -def remove_instance_profiles(connection, module, role_params, role): - role_name = module.params.get('name') - delete_profiles = module.params.get("delete_instance_profile") - - try: - instance_profiles = connection.list_instance_profiles_for_role(aws_retry=True, **role_params)['InstanceProfiles'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(role_name)) - - # Remove the role from the instance profile(s) - for profile in instance_profiles: - profile_name = profile['InstanceProfileName'] - try: - if not module.check_mode: - connection.remove_role_from_instance_profile(aws_retry=True, InstanceProfileName=profile_name, **role_params) - if profile_name == role_name: - if delete_profiles: - try: - connection.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to remove instance profile {0}".format(profile_name)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format(role_name, profile_name)) - - -def destroy_role(connection, module): - - role_name = module.params.get('name') - role = get_role(connection, module, role_name) - role_params = dict() - role_params['RoleName'] = role_name - boundary_params = dict(role_params) - boundary_params['PermissionsBoundary'] = '' - - if role is None: - module.exit_json(changed=False) - - # Before we try to delete the role we need to remove any - # - attached instance profiles - # - attached managed policies - # - permissions boundary - remove_instance_profiles(connection, module, role_params, role) - update_managed_policies(connection, module, role_params, role, [], True) - update_role_permissions_boundary(connection, module, boundary_params, role) - - try: - if not module.check_mode: - connection.delete_role(aws_retry=True, **role_params) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to delete role") - - module.exit_json(changed=True) - - -def get_role_with_backoff(connection, module, name): - try: - return AWSRetry.jittered_backoff(catch_extra_error_codes=['NoSuchEntity'])(connection.get_role)(RoleName=name)['Role'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) - - -def get_role(connection, module, name): - try: - return connection.get_role(RoleName=name, aws_retry=True)['Role'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return None - else: - module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to get role {0}".format(name)) - - -def get_attached_policy_list(connection, module, name): - try: - return connection.list_attached_role_policies(RoleName=name, aws_retry=True)['AttachedPolicies'] - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to list attached policies for role {0}".format(name)) - - -def get_role_tags(connection, module): - role_name = module.params.get('name') - if not hasattr(connection, 'list_role_tags'): - return {} - try: - return boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to list tags for role {0}".format(role_name)) - - -def update_role_tags(connection, module, params, role): - new_tags = params.get('Tags') - if new_tags is None: - return False - new_tags = boto3_tag_list_to_ansible_dict(new_tags) - - role_name = module.params.get('name') - purge_tags = module.params.get('purge_tags') - - try: - existing_tags = boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) - except (ClientError, KeyError): - existing_tags = {} - - tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, new_tags, purge_tags=purge_tags) - - if not module.check_mode: - try: - if tags_to_remove: - connection.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) - if tags_to_add: - connection.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg='Unable to set tags for role %s' % role_name) - - changed = bool(tags_to_add) or bool(tags_to_remove) - return changed - - -def main(): - - argument_spec = dict( - name=dict(type='str', required=True), - path=dict(type='str', default="/"), - assume_role_policy_document=dict(type='json'), - managed_policies=dict(type='list', aliases=['managed_policy']), - max_session_duration=dict(type='int'), - state=dict(type='str', choices=['present', 'absent'], default='present'), - description=dict(type='str'), - boundary=dict(type='str', aliases=['boundary_policy_arn']), - create_instance_profile=dict(type='bool', default=True), - delete_instance_profile=dict(type='bool', default=False), - purge_policies=dict(type='bool', aliases=['purge_policy', 'purge_managed_policies']), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=True), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[('state', 'present', ['assume_role_policy_document'])], - supports_check_mode=True) - - if module.params.get('purge_policies') is None: - module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.' - ' To maintain the existing behaviour explicity set purge_policies=true', version='2.14') - - if module.params.get('boundary'): - if module.params.get('create_instance_profile'): - module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.") - if not module.params.get('boundary').startswith('arn:aws:iam'): - module.fail_json(msg="Boundary policy must be an ARN") - if module.params.get('tags') is not None and not module.botocore_at_least('1.12.46'): - module.fail_json(msg="When managing tags botocore must be at least v1.12.46. " - "Current versions: boto3-{boto3_version} botocore-{botocore_version}".format(**module._gather_versions())) - if module.params.get('boundary') is not None and not module.botocore_at_least('1.10.57'): - module.fail_json(msg="When using a boundary policy, botocore must be at least v1.10.57. " - "Current versions: boto3-{boto3_version} botocore-{botocore_version}".format(**module._gather_versions())) - if module.params.get('max_session_duration'): - max_session_duration = module.params.get('max_session_duration') - if max_session_duration < 3600 or max_session_duration > 43200: - module.fail_json(msg="max_session_duration must be between 1 and 12 hours (3600 and 43200 seconds)") - if module.params.get('path'): - path = module.params.get('path') - if not path.endswith('/') or not path.startswith('/'): - module.fail_json(msg="path must begin and end with /") - - connection = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) - - state = module.params.get("state") - - if state == 'present': - create_or_update_role(connection, module) - else: - destroy_role(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_role_info.py b/lib/ansible/modules/cloud/amazon/iam_role_info.py deleted file mode 100644 index 802870d756..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_role_info.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_role_info -short_description: Gather information on IAM roles -description: - - Gathers information about IAM roles. - - This module was called C(iam_role_facts) before Ansible 2.9. The usage did not change. -version_added: "2.5" -requirements: [ boto3 ] -author: - - "Will Thames (@willthames)" -options: - name: - description: - - Name of a role to search for. - - Mutually exclusive with I(path_prefix). - aliases: - - role_name - type: str - path_prefix: - description: - - Prefix of role to restrict IAM role search for. - - Mutually exclusive with I(name). - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# find all existing IAM roles -- iam_role_info: - register: result - -# describe a single role -- iam_role_info: - name: MyIAMRole - -# describe all roles matching a path prefix -- iam_role_info: - path_prefix: /application/path -''' - -RETURN = ''' -iam_roles: - description: List of IAM roles - returned: always - type: complex - contains: - arn: - description: Amazon Resource Name for IAM role. - returned: always - type: str - sample: arn:aws:iam::123456789012:role/AnsibleTestRole - assume_role_policy_document: - description: Policy Document describing what can assume the role. - returned: always - type: str - create_date: - description: Date IAM role was created. - returned: always - type: str - sample: '2017-10-23T00:05:08+00:00' - inline_policies: - description: List of names of inline policies. - returned: always - type: list - sample: [] - managed_policies: - description: List of attached managed policies. - returned: always - type: complex - contains: - policy_arn: - description: Amazon Resource Name for the policy. - returned: always - type: str - sample: arn:aws:iam::123456789012:policy/AnsibleTestEC2Policy - policy_name: - description: Name of managed policy. - returned: always - type: str - sample: AnsibleTestEC2Policy - instance_profiles: - description: List of attached instance profiles. - returned: always - type: complex - contains: - arn: - description: Amazon Resource Name for the instance profile. - returned: always - type: str - sample: arn:aws:iam::123456789012:instance-profile/AnsibleTestEC2Policy - create_date: - description: Date instance profile was created. - returned: always - type: str - sample: '2017-10-23T00:05:08+00:00' - instance_profile_id: - description: Amazon Identifier for the instance profile. - returned: always - type: str - sample: AROAII7ABCD123456EFGH - instance_profile_name: - description: Name of instance profile. - returned: always - type: str - sample: AnsibleTestEC2Policy - path: - description: Path of instance profile. - returned: always - type: str - sample: / - roles: - description: List of roles associated with this instance profile. - returned: always - type: list - sample: [] - path: - description: Path of role. - returned: always - type: str - sample: / - role_id: - description: Amazon Identifier for the role. - returned: always - type: str - sample: AROAII7ABCD123456EFGH - role_name: - description: Name of the role. - returned: always - type: str - sample: AnsibleTestRole - tags: - description: Role tags. - type: dict - returned: always - sample: '{"Env": "Prod"}' -''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, AWSRetry - - -@AWSRetry.exponential_backoff() -def list_iam_roles_with_backoff(client, **kwargs): - paginator = client.get_paginator('list_roles') - return paginator.paginate(**kwargs).build_full_result() - - -@AWSRetry.exponential_backoff() -def list_iam_role_policies_with_backoff(client, role_name): - paginator = client.get_paginator('list_role_policies') - return paginator.paginate(RoleName=role_name).build_full_result()['PolicyNames'] - - -@AWSRetry.exponential_backoff() -def list_iam_attached_role_policies_with_backoff(client, role_name): - paginator = client.get_paginator('list_attached_role_policies') - return paginator.paginate(RoleName=role_name).build_full_result()['AttachedPolicies'] - - -@AWSRetry.exponential_backoff() -def list_iam_instance_profiles_for_role_with_backoff(client, role_name): - paginator = client.get_paginator('list_instance_profiles_for_role') - return paginator.paginate(RoleName=role_name).build_full_result()['InstanceProfiles'] - - -def describe_iam_role(module, client, role): - name = role['RoleName'] - try: - role['InlinePolicies'] = list_iam_role_policies_with_backoff(client, name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get inline policies for role %s" % name) - try: - role['ManagedPolicies'] = list_iam_attached_role_policies_with_backoff(client, name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get managed policies for role %s" % name) - try: - role['InstanceProfiles'] = list_iam_instance_profiles_for_role_with_backoff(client, name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get instance profiles for role %s" % name) - try: - role['tags'] = boto3_tag_list_to_ansible_dict(role['Tags']) - del role['Tags'] - except KeyError: - role['tags'] = {} - return role - - -def describe_iam_roles(module, client): - name = module.params['name'] - path_prefix = module.params['path_prefix'] - if name: - try: - roles = [client.get_role(RoleName=name)['Role']] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return [] - else: - module.fail_json_aws(e, msg="Couldn't get IAM role %s" % name) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Couldn't get IAM role %s" % name) - else: - params = dict() - if path_prefix: - if not path_prefix.startswith('/'): - path_prefix = '/' + path_prefix - if not path_prefix.endswith('/'): - path_prefix = path_prefix + '/' - params['PathPrefix'] = path_prefix - try: - roles = list_iam_roles_with_backoff(client, **params)['Roles'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't list IAM roles") - return [camel_dict_to_snake_dict(describe_iam_role(module, client, role), ignore_list=['tags']) for role in roles] - - -def main(): - """ - Module action handler - """ - argument_spec = dict( - name=dict(aliases=['role_name']), - path_prefix=dict(), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['name', 'path_prefix']]) - if module._name == 'iam_role_facts': - module.deprecate("The 'iam_role_facts' module has been renamed to 'iam_role_info'", version='2.13') - - client = module.client('iam') - - module.exit_json(changed=False, iam_roles=describe_iam_roles(module, client)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_saml_federation.py b/lib/ansible/modules/cloud/amazon/iam_saml_federation.py deleted file mode 100644 index ee3c720afb..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_saml_federation.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_saml_federation -version_added: "2.10" -short_description: Maintain IAM SAML federation configuration. -requirements: - - boto3 -description: - - Provides a mechanism to manage AWS IAM SAML Identity Federation providers (create/update/delete metadata). -options: - name: - description: - - The name of the provider to create. - required: true - type: str - saml_metadata_document: - description: - - The XML document generated by an identity provider (IdP) that supports SAML 2.0. - type: str - state: - description: - - Whether to create or delete identity provider. If 'present' is specified it will attempt to update the identity provider matching the name field. - default: present - choices: [ "present", "absent" ] - type: str -extends_documentation_fragment: - - aws - - ec2 -author: - - Tony (@axc450) - - Aidan Rowe (@aidan-) -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -# It is assumed that their matching environment variables are set. -# Creates a new iam saml identity provider if not present -- name: saml provider - iam_saml_federation: - name: example1 - # the > below opens an indented block, so no escaping/quoting is needed when in the indentation level under this key - saml_metadata_document: > - <?xml version="1.0"?>... - <md:EntityDescriptor -# Creates a new iam saml identity provider if not present -- name: saml provider - iam_saml_federation: - name: example2 - saml_metadata_document: "{{ item }}" - with_file: /path/to/idp/metdata.xml -# Removes iam saml identity provider -- name: remove saml provider - iam_saml_federation: - name: example3 - state: absent -''' - -RETURN = ''' -saml_provider: - description: Details of the SAML Identity Provider that was created/modified. - type: complex - returned: present - contains: - arn: - description: The ARN of the identity provider. - type: str - returned: present - sample: "arn:aws:iam::123456789012:saml-provider/my_saml_provider" - metadata_document: - description: The XML metadata document that includes information about an identity provider. - type: str - returned: present - create_date: - description: The date and time when the SAML provider was created in ISO 8601 date-time format. - type: str - returned: present - sample: "2017-02-08T04:36:28+00:00" - expire_date: - description: The expiration date and time for the SAML provider in ISO 8601 date-time format. - type: str - returned: present - sample: "2017-02-08T04:36:28+00:00" -''' - -try: - import botocore.exceptions -except ImportError: - pass - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry - - -class SAMLProviderManager: - """Handles SAML Identity Provider configuration""" - - def __init__(self, module): - self.module = module - - try: - self.conn = module.client('iam') - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Unknown boto error") - - # use retry decorator for boto3 calls - @AWSRetry.backoff(tries=3, delay=5) - def _list_saml_providers(self): - return self.conn.list_saml_providers() - - @AWSRetry.backoff(tries=3, delay=5) - def _get_saml_provider(self, arn): - return self.conn.get_saml_provider(SAMLProviderArn=arn) - - @AWSRetry.backoff(tries=3, delay=5) - def _update_saml_provider(self, arn, metadata): - return self.conn.update_saml_provider(SAMLProviderArn=arn, SAMLMetadataDocument=metadata) - - @AWSRetry.backoff(tries=3, delay=5) - def _create_saml_provider(self, metadata, name): - return self.conn.create_saml_provider(SAMLMetadataDocument=metadata, Name=name) - - @AWSRetry.backoff(tries=3, delay=5) - def _delete_saml_provider(self, arn): - return self.conn.delete_saml_provider(SAMLProviderArn=arn) - - def _get_provider_arn(self, name): - providers = self._list_saml_providers() - for p in providers['SAMLProviderList']: - provider_name = p['Arn'].split('/', 1)[1] - if name == provider_name: - return p['Arn'] - - return None - - def create_or_update_saml_provider(self, name, metadata): - if not metadata: - self.module.fail_json(msg="saml_metadata_document must be defined for present state") - - res = {'changed': False} - try: - arn = self._get_provider_arn(name) - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not get the ARN of the identity provider '{0}'".format(name)) - - if arn: # see if metadata needs updating - try: - resp = self._get_saml_provider(arn) - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not retrieve the identity provider '{0}'".format(name)) - - if metadata.strip() != resp['SAMLMetadataDocument'].strip(): - # provider needs updating - res['changed'] = True - if not self.module.check_mode: - try: - resp = self._update_saml_provider(arn, metadata) - res['saml_provider'] = self._build_res(resp['SAMLProviderArn']) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Could not update the identity provider '{0}'".format(name)) - - else: # create - res['changed'] = True - if not self.module.check_mode: - try: - resp = self._create_saml_provider(metadata, name) - res['saml_provider'] = self._build_res(resp['SAMLProviderArn']) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Could not create the identity provider '{0}'".format(name)) - - self.module.exit_json(**res) - - def delete_saml_provider(self, name): - res = {'changed': False} - try: - arn = self._get_provider_arn(name) - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Could not get the ARN of the identity provider '{0}'".format(name)) - - if arn: # delete - res['changed'] = True - if not self.module.check_mode: - try: - self._delete_saml_provider(arn) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Could not delete the identity provider '{0}'".format(name)) - - self.module.exit_json(**res) - - def _build_res(self, arn): - saml_provider = self._get_saml_provider(arn) - return { - "arn": arn, - "metadata_document": saml_provider["SAMLMetadataDocument"], - "create_date": saml_provider["CreateDate"].isoformat(), - "expire_date": saml_provider["ValidUntil"].isoformat() - } - - -def main(): - argument_spec = dict( - name=dict(required=True), - saml_metadata_document=dict(default=None, required=False), - state=dict(default='present', required=False, choices=['present', 'absent']), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_if=[('state', 'present', ['saml_metadata_document'])] - ) - - name = module.params['name'] - state = module.params.get('state') - saml_metadata_document = module.params.get('saml_metadata_document') - - sp_man = SAMLProviderManager(module) - - if state == 'present': - sp_man.create_or_update_saml_provider(name, saml_metadata_document) - elif state == 'absent': - sp_man.delete_saml_provider(name) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_server_certificate_info.py b/lib/ansible/modules/cloud/amazon/iam_server_certificate_info.py deleted file mode 100644 index a7970371c5..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_server_certificate_info.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: iam_server_certificate_info -short_description: Retrieve the information of a server certificate -description: - - Retrieve the attributes of a server certificate. - - This module was called C(iam_server_certificate_facts) before Ansible 2.9. The usage did not change. -version_added: "2.2" -author: "Allen Sanabria (@linuxdynasty)" -requirements: [boto3, botocore] -options: - name: - description: - - The name of the server certificate you are retrieving attributes for. - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Retrieve server certificate -- iam_server_certificate_info: - name: production-cert - register: server_cert - -# Fail if the server certificate name was not found -- iam_server_certificate_info: - name: production-cert - register: server_cert - failed_when: "{{ server_cert.results | length == 0 }}" -''' - -RETURN = ''' -server_certificate_id: - description: The 21 character certificate id - returned: success - type: str - sample: "ADWAJXWTZAXIPIMQHMJPO" -certificate_body: - description: The asn1der encoded PEM string - returned: success - type: str - sample: "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----" -server_certificate_name: - description: The name of the server certificate - returned: success - type: str - sample: "server-cert-name" -arn: - description: The Amazon resource name of the server certificate - returned: success - type: str - sample: "arn:aws:iam::911277865346:server-certificate/server-cert-name" -path: - description: The path of the server certificate - returned: success - type: str - sample: "/" -expiration: - description: The date and time this server certificate will expire, in ISO 8601 format. - returned: success - type: str - sample: "2017-06-15T12:00:00+00:00" -upload_date: - description: The date and time this server certificate was uploaded, in ISO 8601 format. - returned: success - type: str - sample: "2015-04-25T00:36:40+00:00" -''' - - -try: - import boto3 - import botocore.exceptions - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info - - -def get_server_certs(iam, name=None): - """Retrieve the attributes of a server certificate if it exists or all certs. - Args: - iam (botocore.client.IAM): The boto3 iam instance. - - Kwargs: - name (str): The name of the server certificate. - - Basic Usage: - >>> import boto3 - >>> iam = boto3.client('iam') - >>> name = "server-cert-name" - >>> results = get_server_certs(iam, name) - { - "upload_date": "2015-04-25T00:36:40+00:00", - "server_certificate_id": "ADWAJXWTZAXIPIMQHMJPO", - "certificate_body": "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----", - "server_certificate_name": "server-cert-name", - "expiration": "2017-06-15T12:00:00+00:00", - "path": "/", - "arn": "arn:aws:iam::911277865346:server-certificate/server-cert-name" - } - """ - results = dict() - try: - if name: - server_certs = [iam.get_server_certificate(ServerCertificateName=name)['ServerCertificate']] - else: - server_certs = iam.list_server_certificates()['ServerCertificateMetadataList'] - - for server_cert in server_certs: - if not name: - server_cert = iam.get_server_certificate(ServerCertificateName=server_cert['ServerCertificateName'])['ServerCertificate'] - cert_md = server_cert['ServerCertificateMetadata'] - results[cert_md['ServerCertificateName']] = { - 'certificate_body': server_cert['CertificateBody'], - 'server_certificate_id': cert_md['ServerCertificateId'], - 'server_certificate_name': cert_md['ServerCertificateName'], - 'arn': cert_md['Arn'], - 'path': cert_md['Path'], - 'expiration': cert_md['Expiration'].isoformat(), - 'upload_date': cert_md['UploadDate'].isoformat(), - } - - except botocore.exceptions.ClientError: - pass - - return results - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - name=dict(type='str'), - )) - - module = AnsibleModule(argument_spec=argument_spec,) - if module._name == 'iam_server_certificate_facts': - module.deprecate("The 'iam_server_certificate_facts' module has been renamed to 'iam_server_certificate_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - iam = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) - - cert_name = module.params.get('name') - results = get_server_certs(iam, cert_name) - module.exit_json(results=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_user.py b/lib/ansible/modules/cloud/amazon/iam_user.py deleted file mode 100644 index a85cd94c98..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_user.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: iam_user -short_description: Manage AWS IAM users -description: - - Manage AWS IAM users. -version_added: "2.5" -author: Josh Souza (@joshsouza) -options: - name: - description: - - The name of the user to create. - required: true - type: str - managed_policies: - description: - - A list of managed policy ARNs or friendly names to attach to the user. - - To embed an inline policy, use M(iam_policy). - required: false - type: list - aliases: ['managed_policy'] - state: - description: - - Create or remove the IAM user. - required: true - choices: [ 'present', 'absent' ] - type: str - purge_policies: - description: - - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. - required: false - default: false - type: bool - aliases: ['purge_policy', 'purge_managed_policies'] -requirements: [ botocore, boto3 ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Note: This module does not allow management of groups that users belong to. -# Groups should manage their membership directly using `iam_group`, -# as users belong to them. - -# Create a user -- iam_user: - name: testuser1 - state: present - -# Create a user and attach a managed policy using its ARN -- iam_user: - name: testuser1 - managed_policies: - - arn:aws:iam::aws:policy/AmazonSNSFullAccess - state: present - -# Remove all managed policies from an existing user with an empty list -- iam_user: - name: testuser1 - state: present - purge_policies: true - -# Delete the user -- iam_user: - name: testuser1 - state: absent - -''' -RETURN = ''' -user: - description: dictionary containing all the user information - returned: success - type: complex - contains: - arn: - description: the Amazon Resource Name (ARN) specifying the user - type: str - sample: "arn:aws:iam::1234567890:user/testuser1" - create_date: - description: the date and time, in ISO 8601 date-time format, when the user was created - type: str - sample: "2017-02-08T04:36:28+00:00" - user_id: - description: the stable and unique string identifying the user - type: str - sample: AGPAIDBWE12NSFINE55TM - user_name: - description: the friendly name that identifies the user - type: str - sample: testuser1 - path: - description: the path to the user - type: str - sample: / -''' - -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -import traceback - -try: - from botocore.exceptions import ClientError, ParamValidationError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - - -def compare_attached_policies(current_attached_policies, new_attached_policies): - - # If new_attached_policies is None it means we want to remove all policies - if len(current_attached_policies) > 0 and new_attached_policies is None: - return False - - current_attached_policies_arn_list = [] - for policy in current_attached_policies: - current_attached_policies_arn_list.append(policy['PolicyArn']) - - if not set(current_attached_policies_arn_list).symmetric_difference(set(new_attached_policies)): - return True - else: - return False - - -def convert_friendly_names_to_arns(connection, module, policy_names): - - # List comprehension that looks for any policy in the 'policy_names' list - # that does not begin with 'arn'. If there aren't any, short circuit. - # If there are, translate friendly name to the full arn - if not any([not policy.startswith('arn:') for policy in policy_names if policy is not None]): - return policy_names - allpolicies = {} - paginator = connection.get_paginator('list_policies') - policies = paginator.paginate().build_full_result()['Policies'] - - for policy in policies: - allpolicies[policy['PolicyName']] = policy['Arn'] - allpolicies[policy['Arn']] = policy['Arn'] - try: - return [allpolicies[policy] for policy in policy_names] - except KeyError as e: - module.fail_json(msg="Couldn't find policy: " + str(e)) - - -def create_or_update_user(connection, module): - - params = dict() - params['UserName'] = module.params.get('name') - managed_policies = module.params.get('managed_policies') - purge_policies = module.params.get('purge_policies') - changed = False - if managed_policies: - managed_policies = convert_friendly_names_to_arns(connection, module, managed_policies) - - # Get user - user = get_user(connection, module, params['UserName']) - - # If user is None, create it - if user is None: - # Check mode means we would create the user - if module.check_mode: - module.exit_json(changed=True) - - try: - connection.create_user(**params) - changed = True - except ClientError as e: - module.fail_json(msg="Unable to create user: {0}".format(to_native(e)), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except ParamValidationError as e: - module.fail_json(msg="Unable to create user: {0}".format(to_native(e)), exception=traceback.format_exc()) - - # Manage managed policies - current_attached_policies = get_attached_policy_list(connection, module, params['UserName']) - if not compare_attached_policies(current_attached_policies, managed_policies): - current_attached_policies_arn_list = [] - for policy in current_attached_policies: - current_attached_policies_arn_list.append(policy['PolicyArn']) - - # If managed_policies has a single empty element we want to remove all attached policies - if purge_policies: - # Detach policies not present - for policy_arn in list(set(current_attached_policies_arn_list) - set(managed_policies)): - changed = True - if not module.check_mode: - try: - connection.detach_user_policy(UserName=params['UserName'], PolicyArn=policy_arn) - except ClientError as e: - module.fail_json(msg="Unable to detach policy {0} from user {1}: {2}".format( - policy_arn, params['UserName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except ParamValidationError as e: - module.fail_json(msg="Unable to detach policy {0} from user {1}: {2}".format( - policy_arn, params['UserName'], to_native(e)), - exception=traceback.format_exc()) - - # If there are policies to adjust that aren't in the current list, then things have changed - # Otherwise the only changes were in purging above - if set(managed_policies).difference(set(current_attached_policies_arn_list)): - changed = True - # If there are policies in managed_policies attach each policy - if managed_policies != [None] and not module.check_mode: - for policy_arn in managed_policies: - try: - connection.attach_user_policy(UserName=params['UserName'], PolicyArn=policy_arn) - except ClientError as e: - module.fail_json(msg="Unable to attach policy {0} to user {1}: {2}".format( - policy_arn, params['UserName'], to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except ParamValidationError as e: - module.fail_json(msg="Unable to attach policy {0} to user {1}: {2}".format( - policy_arn, params['UserName'], to_native(e)), - exception=traceback.format_exc()) - if module.check_mode: - module.exit_json(changed=changed) - - # Get the user again - user = get_user(connection, module, params['UserName']) - - module.exit_json(changed=changed, iam_user=camel_dict_to_snake_dict(user)) - - -def destroy_user(connection, module): - - user_name = module.params.get('name') - - user = get_user(connection, module, user_name) - # User is not present - if not user: - module.exit_json(changed=False) - - # Check mode means we would remove this user - if module.check_mode: - module.exit_json(changed=True) - - # Remove any attached policies otherwise deletion fails - try: - for policy in get_attached_policy_list(connection, module, user_name): - connection.detach_user_policy(UserName=user_name, PolicyArn=policy['PolicyArn']) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to delete user {0}".format(user_name)) - - try: - # Remove user's access keys - access_keys = connection.list_access_keys(UserName=user_name)["AccessKeyMetadata"] - for access_key in access_keys: - connection.delete_access_key(UserName=user_name, AccessKeyId=access_key["AccessKeyId"]) - - # Remove user's login profile (console password) - delete_user_login_profile(connection, module, user_name) - - # Remove user's ssh public keys - ssh_public_keys = connection.list_ssh_public_keys(UserName=user_name)["SSHPublicKeys"] - for ssh_public_key in ssh_public_keys: - connection.delete_ssh_public_key(UserName=user_name, SSHPublicKeyId=ssh_public_key["SSHPublicKeyId"]) - - # Remove user's service specific credentials - service_credentials = connection.list_service_specific_credentials(UserName=user_name)["ServiceSpecificCredentials"] - for service_specific_credential in service_credentials: - connection.delete_service_specific_credential( - UserName=user_name, - ServiceSpecificCredentialId=service_specific_credential["ServiceSpecificCredentialId"] - ) - - # Remove user's signing certificates - signing_certificates = connection.list_signing_certificates(UserName=user_name)["Certificates"] - for signing_certificate in signing_certificates: - connection.delete_signing_certificate( - UserName=user_name, - CertificateId=signing_certificate["CertificateId"] - ) - - # Remove user's MFA devices - mfa_devices = connection.list_mfa_devices(UserName=user_name)["MFADevices"] - for mfa_device in mfa_devices: - connection.deactivate_mfa_device(UserName=user_name, SerialNumber=mfa_device["SerialNumber"]) - - # Remove user's inline policies - inline_policies = connection.list_user_policies(UserName=user_name)["PolicyNames"] - for policy_name in inline_policies: - connection.delete_user_policy(UserName=user_name, PolicyName=policy_name) - - # Remove user's group membership - user_groups = connection.list_groups_for_user(UserName=user_name)["Groups"] - for group in user_groups: - connection.remove_user_from_group(UserName=user_name, GroupName=group["GroupName"]) - - connection.delete_user(UserName=user_name) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to delete user {0}".format(user_name)) - - module.exit_json(changed=True) - - -def get_user(connection, module, name): - - params = dict() - params['UserName'] = name - - try: - return connection.get_user(**params) - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return None - else: - module.fail_json(msg="Unable to get user {0}: {1}".format(name, to_native(e)), - **camel_dict_to_snake_dict(e.response)) - - -def get_attached_policy_list(connection, module, name): - - try: - return connection.list_attached_user_policies(UserName=name)['AttachedPolicies'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return None - else: - module.fail_json_aws(e, msg="Unable to get policies for user {0}".format(name)) - - -def delete_user_login_profile(connection, module, user_name): - - try: - return connection.delete_login_profile(UserName=user_name) - except ClientError as e: - if e.response["Error"]["Code"] == "NoSuchEntity": - return None - else: - module.fail_json_aws(e, msg="Unable to delete login profile for user {0}".format(user_name)) - - -def main(): - - argument_spec = dict( - name=dict(required=True, type='str'), - managed_policies=dict(default=[], type='list', aliases=['managed_policy']), - state=dict(choices=['present', 'absent'], required=True), - purge_policies=dict(default=False, type='bool', aliases=['purge_policy', 'purge_managed_policies']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - connection = module.client('iam') - - state = module.params.get("state") - - if state == 'present': - create_or_update_user(connection, module) - else: - destroy_user(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/iam_user_info.py b/lib/ansible/modules/cloud/amazon/iam_user_info.py deleted file mode 100644 index c7a25409b7..0000000000 --- a/lib/ansible/modules/cloud/amazon/iam_user_info.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- -# Copyright: (c) 2018, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - - -DOCUMENTATION = ''' ---- -module: iam_user_info -short_description: Gather IAM user(s) facts in AWS -description: - - This module can be used to gather IAM user(s) facts in AWS. -version_added: "2.10" -author: - - Constantin Bugneac (@Constantin07) - - Abhijeet Kasurde (@Akasurde) -options: - name: - description: - - The name of the IAM user to look for. - required: false - type: str - group: - description: - - The group name name of the IAM user to look for. Mutually exclusive with C(path). - required: false - type: str - path: - description: - - The path to the IAM user. Mutually exclusive with C(group). - - If specified, then would get all user names whose path starts with user provided value. - required: false - default: '/' - type: str -requirements: - - botocore - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = r''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Gather facts about "test" user. -- name: Get IAM user facts - iam_user_info: - name: "test" - -# Gather facts about all users in the "dev" group. -- name: Get IAM user facts - iam_user_info: - group: "dev" - -# Gather facts about all users with "/division_abc/subdivision_xyz/" path. -- name: Get IAM user facts - iam_user_info: - path: "/division_abc/subdivision_xyz/" -''' - -RETURN = r''' -iam_users: - description: list of maching iam users - returned: success - type: complex - contains: - arn: - description: the ARN of the user - returned: if user exists - type: str - sample: "arn:aws:iam::156360693172:user/dev/test_user" - create_date: - description: the datetime user was created - returned: if user exists - type: str - sample: "2016-05-24T12:24:59+00:00" - password_last_used: - description: the last datetime the password was used by user - returned: if password was used at least once - type: str - sample: "2016-05-25T13:39:11+00:00" - path: - description: the path to user - returned: if user exists - type: str - sample: "/dev/" - user_id: - description: the unique user id - returned: if user exists - type: str - sample: "AIDUIOOCQKTUGI6QJLGH2" - user_name: - description: the user name - returned: if user exists - type: str - sample: "test_user" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -@AWSRetry.exponential_backoff() -def list_iam_users_with_backoff(client, operation, **kwargs): - paginator = client.get_paginator(operation) - return paginator.paginate(**kwargs).build_full_result() - - -def list_iam_users(connection, module): - - name = module.params.get('name') - group = module.params.get('group') - path = module.params.get('path') - - params = dict() - iam_users = [] - - if not group and not path: - if name: - params['UserName'] = name - try: - iam_users.append(connection.get_user(**params)['User']) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get IAM user info for user %s" % name) - - if group: - params['GroupName'] = group - try: - iam_users = list_iam_users_with_backoff(connection, 'get_group', **params)['Users'] - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get IAM user info for group %s" % group) - if name: - iam_users = [user for user in iam_users if user['UserName'] == name] - - if path and not group: - params['PathPrefix'] = path - try: - iam_users = list_iam_users_with_backoff(connection, 'list_users', **params)['Users'] - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get IAM user info for path %s" % path) - if name: - iam_users = [user for user in iam_users if user['UserName'] == name] - - module.exit_json(iam_users=[camel_dict_to_snake_dict(user) for user in iam_users]) - - -def main(): - argument_spec = dict( - name=dict(), - group=dict(), - path=dict(default='/') - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['group', 'path'] - ], - supports_check_mode=True - ) - - connection = module.client('iam') - - list_iam_users(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/kinesis_stream.py b/lib/ansible/modules/cloud/amazon/kinesis_stream.py deleted file mode 100644 index c924616aad..0000000000 --- a/lib/ansible/modules/cloud/amazon/kinesis_stream.py +++ /dev/null @@ -1,1428 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: kinesis_stream -short_description: Manage a Kinesis Stream. -description: - - Create or Delete a Kinesis Stream. - - Update the retention period of a Kinesis Stream. - - Update Tags on a Kinesis Stream. - - Enable/disable server side encryption on a Kinesis Stream. -version_added: "2.2" -requirements: [ boto3 ] -author: Allen Sanabria (@linuxdynasty) -options: - name: - description: - - The name of the Kinesis Stream you are managing. - required: true - type: str - shards: - description: - - The number of shards you want to have with this stream. - - This is required when I(state=present) - type: int - retention_period: - description: - - The length of time (in hours) data records are accessible after they are added to - the stream. - - The default retention period is 24 hours and can not be less than 24 hours. - - The maximum retention period is 168 hours. - - The retention period can be modified during any point in time. - type: int - state: - description: - - Create or Delete the Kinesis Stream. - default: present - choices: [ 'present', 'absent' ] - type: str - wait: - description: - - Wait for operation to complete before returning. - default: true - type: bool - wait_timeout: - description: - - How many seconds to wait for an operation to complete before timing out. - default: 300 - type: int - tags: - description: - - "A dictionary of resource tags of the form: C({ tag1: value1, tag2: value2 })." - aliases: [ "resource_tags" ] - type: dict - encryption_state: - description: - - Enable or Disable encryption on the Kinesis Stream. - choices: [ 'enabled', 'disabled' ] - version_added: "2.5" - type: str - encryption_type: - description: - - The type of encryption. - - Defaults to C(KMS) - choices: ['KMS', 'NONE'] - version_added: "2.5" - type: str - key_id: - description: - - The GUID or alias for the KMS key. - version_added: "2.5" - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic creation example: -- name: Set up Kinesis Stream with 10 shards and wait for the stream to become ACTIVE - kinesis_stream: - name: test-stream - shards: 10 - wait: yes - wait_timeout: 600 - register: test_stream - -# Basic creation example with tags: -- name: Set up Kinesis Stream with 10 shards, tag the environment, and wait for the stream to become ACTIVE - kinesis_stream: - name: test-stream - shards: 10 - tags: - Env: development - wait: yes - wait_timeout: 600 - register: test_stream - -# Basic creation example with tags and increase the retention period from the default 24 hours to 48 hours: -- name: Set up Kinesis Stream with 10 shards, tag the environment, increase the retention period and wait for the stream to become ACTIVE - kinesis_stream: - name: test-stream - retention_period: 48 - shards: 10 - tags: - Env: development - wait: yes - wait_timeout: 600 - register: test_stream - -# Basic delete example: -- name: Delete Kinesis Stream test-stream and wait for it to finish deleting. - kinesis_stream: - name: test-stream - state: absent - wait: yes - wait_timeout: 600 - register: test_stream - -# Basic enable encryption example: -- name: Encrypt Kinesis Stream test-stream. - kinesis_stream: - name: test-stream - state: present - encryption_state: enabled - encryption_type: KMS - key_id: alias/aws/kinesis - wait: yes - wait_timeout: 600 - register: test_stream - -# Basic disable encryption example: -- name: Encrypt Kinesis Stream test-stream. - kinesis_stream: - name: test-stream - state: present - encryption_state: disabled - encryption_type: KMS - key_id: alias/aws/kinesis - wait: yes - wait_timeout: 600 - register: test_stream -''' - -RETURN = ''' -stream_name: - description: The name of the Kinesis Stream. - returned: when state == present. - type: str - sample: "test-stream" -stream_arn: - description: The amazon resource identifier - returned: when state == present. - type: str - sample: "arn:aws:kinesis:east-side:123456789:stream/test-stream" -stream_status: - description: The current state of the Kinesis Stream. - returned: when state == present. - type: str - sample: "ACTIVE" -retention_period_hours: - description: Number of hours messages will be kept for a Kinesis Stream. - returned: when state == present. - type: int - sample: 24 -tags: - description: Dictionary containing all the tags associated with the Kinesis stream. - returned: when state == present. - type: dict - sample: { - "Name": "Splunk", - "Env": "development" - } -''' - -import re -import datetime -import time -from functools import reduce - -try: - import botocore.exceptions -except ImportError: - pass # Taken care of by ec2.HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, boto3_conn, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils._text import to_native - - -def convert_to_lower(data): - """Convert all uppercase keys in dict with lowercase_ - Args: - data (dict): Dictionary with keys that have upper cases in them - Example.. FooBar == foo_bar - if a val is of type datetime.datetime, it will be converted to - the ISO 8601 - - Basic Usage: - >>> test = {'FooBar': []} - >>> test = convert_to_lower(test) - { - 'foo_bar': [] - } - - Returns: - Dictionary - """ - results = dict() - if isinstance(data, dict): - for key, val in data.items(): - key = re.sub(r'(([A-Z]{1,3}){1})', r'_\1', key).lower() - if key[0] == '_': - key = key[1:] - if isinstance(val, datetime.datetime): - results[key] = val.isoformat() - elif isinstance(val, dict): - results[key] = convert_to_lower(val) - elif isinstance(val, list): - converted = list() - for item in val: - converted.append(convert_to_lower(item)) - results[key] = converted - else: - results[key] = val - return results - - -def make_tags_in_proper_format(tags): - """Take a dictionary of tags and convert them into the AWS Tags format. - Args: - tags (list): The tags you want applied. - - Basic Usage: - >>> tags = [{'Key': 'env', 'Value': 'development'}] - >>> make_tags_in_proper_format(tags) - { - "env": "development", - } - - Returns: - Dict - """ - formatted_tags = dict() - for tag in tags: - formatted_tags[tag.get('Key')] = tag.get('Value') - - return formatted_tags - - -def make_tags_in_aws_format(tags): - """Take a dictionary of tags and convert them into the AWS Tags format. - Args: - tags (dict): The tags you want applied. - - Basic Usage: - >>> tags = {'env': 'development', 'service': 'web'} - >>> make_tags_in_proper_format(tags) - [ - { - "Value": "web", - "Key": "service" - }, - { - "Value": "development", - "key": "env" - } - ] - - Returns: - List - """ - formatted_tags = list() - for key, val in tags.items(): - formatted_tags.append({ - 'Key': key, - 'Value': val - }) - - return formatted_tags - - -def get_tags(client, stream_name, check_mode=False): - """Retrieve the tags for a Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): Name of the Kinesis stream. - - Kwargs: - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >> get_tags(client, stream_name) - - Returns: - Tuple (bool, str, dict) - """ - err_msg = '' - success = False - params = { - 'StreamName': stream_name, - } - results = dict() - try: - if not check_mode: - results = ( - client.list_tags_for_stream(**params)['Tags'] - ) - else: - results = [ - { - 'Key': 'DryRunMode', - 'Value': 'true' - }, - ] - success = True - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg, results - - -def find_stream(client, stream_name, check_mode=False): - """Retrieve a Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): Name of the Kinesis stream. - - Kwargs: - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - - Returns: - Tuple (bool, str, dict) - """ - err_msg = '' - success = False - params = { - 'StreamName': stream_name, - } - results = dict() - has_more_shards = True - shards = list() - try: - if not check_mode: - while has_more_shards: - results = ( - client.describe_stream(**params)['StreamDescription'] - ) - shards.extend(results.pop('Shards')) - has_more_shards = results['HasMoreShards'] - results['Shards'] = shards - num_closed_shards = len([s for s in shards if 'EndingSequenceNumber' in s['SequenceNumberRange']]) - results['OpenShardsCount'] = len(shards) - num_closed_shards - results['ClosedShardsCount'] = num_closed_shards - results['ShardsCount'] = len(shards) - else: - results = { - 'OpenShardsCount': 5, - 'ClosedShardsCount': 0, - 'ShardsCount': 5, - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': stream_name, - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/{0}'.format(stream_name), - 'StreamStatus': 'ACTIVE', - 'EncryptionType': 'NONE' - } - success = True - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg, results - - -def wait_for_status(client, stream_name, status, wait_timeout=300, - check_mode=False): - """Wait for the status to change for a Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client - stream_name (str): The name of the kinesis stream. - status (str): The status to wait for. - examples. status=available, status=deleted - - Kwargs: - wait_timeout (int): Number of seconds to wait, until this timeout is reached. - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> wait_for_status(client, stream_name, 'ACTIVE', 300) - - Returns: - Tuple (bool, str, dict) - """ - polling_increment_secs = 5 - wait_timeout = time.time() + wait_timeout - status_achieved = False - stream = dict() - err_msg = "" - - while wait_timeout > time.time(): - try: - find_success, find_msg, stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if check_mode: - status_achieved = True - break - - elif status != 'DELETING': - if find_success and stream: - if stream.get('StreamStatus') == status: - status_achieved = True - break - - else: - if not find_success: - status_achieved = True - break - - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - time.sleep(polling_increment_secs) - - if not status_achieved: - err_msg = "Wait time out reached, while waiting for results" - else: - err_msg = "Status {0} achieved successfully".format(status) - - return status_achieved, err_msg, stream - - -def tags_action(client, stream_name, tags, action='create', check_mode=False): - """Create or delete multiple tags from a Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - resource_id (str): The Amazon resource id. - tags (list): List of dictionaries. - examples.. [{Name: "", Values: [""]}] - - Kwargs: - action (str): The action to perform. - valid actions == create and delete - default=create - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('ec2') - >>> resource_id = 'pcx-123345678' - >>> tags = {'env': 'development'} - >>> update_tags(client, resource_id, tags) - [True, ''] - - Returns: - List (bool, str) - """ - success = False - err_msg = "" - params = {'StreamName': stream_name} - try: - if not check_mode: - if action == 'create': - params['Tags'] = tags - client.add_tags_to_stream(**params) - success = True - elif action == 'delete': - params['TagKeys'] = list(tags) - client.remove_tags_from_stream(**params) - success = True - else: - err_msg = 'Invalid action {0}'.format(action) - else: - if action == 'create': - success = True - elif action == 'delete': - success = True - else: - err_msg = 'Invalid action {0}'.format(action) - - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg - - -def recreate_tags_from_list(list_of_tags): - """Recreate tags from a list of tuples into the Amazon Tag format. - Args: - list_of_tags (list): List of tuples. - - Basic Usage: - >>> list_of_tags = [('Env', 'Development')] - >>> recreate_tags_from_list(list_of_tags) - [ - { - "Value": "Development", - "Key": "Env" - } - ] - - Returns: - List - """ - tags = list() - i = 0 - for i in range(len(list_of_tags)): - key_name = list_of_tags[i][0] - key_val = list_of_tags[i][1] - tags.append( - { - 'Key': key_name, - 'Value': key_val - } - ) - return tags - - -def update_tags(client, stream_name, tags, check_mode=False): - """Update tags for an amazon resource. - Args: - resource_id (str): The Amazon resource id. - tags (dict): Dictionary of tags you want applied to the Kinesis stream. - - Kwargs: - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('ec2') - >>> stream_name = 'test-stream' - >>> tags = {'env': 'development'} - >>> update_tags(client, stream_name, tags) - [True, ''] - - Return: - Tuple (bool, str) - """ - success = False - changed = False - err_msg = '' - tag_success, tag_msg, current_tags = ( - get_tags(client, stream_name, check_mode=check_mode) - ) - if current_tags: - tags = make_tags_in_aws_format(tags) - current_tags_set = ( - set( - reduce( - lambda x, y: x + y, - [make_tags_in_proper_format(current_tags).items()] - ) - ) - ) - - new_tags_set = ( - set( - reduce( - lambda x, y: x + y, - [make_tags_in_proper_format(tags).items()] - ) - ) - ) - tags_to_delete = list(current_tags_set.difference(new_tags_set)) - tags_to_update = list(new_tags_set.difference(current_tags_set)) - if tags_to_delete: - tags_to_delete = make_tags_in_proper_format( - recreate_tags_from_list(tags_to_delete) - ) - delete_success, delete_msg = ( - tags_action( - client, stream_name, tags_to_delete, action='delete', - check_mode=check_mode - ) - ) - if not delete_success: - return delete_success, changed, delete_msg - if tags_to_update: - tags = make_tags_in_proper_format( - recreate_tags_from_list(tags_to_update) - ) - else: - return True, changed, 'Tags do not need to be updated' - - if tags: - create_success, create_msg = ( - tags_action( - client, stream_name, tags, action='create', - check_mode=check_mode - ) - ) - if create_success: - changed = True - return create_success, changed, create_msg - - return success, changed, err_msg - - -def stream_action(client, stream_name, shard_count=1, action='create', - timeout=300, check_mode=False): - """Create or Delete an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - shard_count (int): Number of shards this stream will use. - action (str): The action to perform. - valid actions == create and delete - default=create - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> shard_count = 20 - >>> stream_action(client, stream_name, shard_count, action='create') - - Returns: - List (bool, str) - """ - success = False - err_msg = '' - params = { - 'StreamName': stream_name - } - try: - if not check_mode: - if action == 'create': - params['ShardCount'] = shard_count - client.create_stream(**params) - success = True - elif action == 'delete': - client.delete_stream(**params) - success = True - else: - err_msg = 'Invalid action {0}'.format(action) - else: - if action == 'create': - success = True - elif action == 'delete': - success = True - else: - err_msg = 'Invalid action {0}'.format(action) - - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg - - -def stream_encryption_action(client, stream_name, action='start_encryption', encryption_type='', key_id='', - timeout=300, check_mode=False): - """Create, Encrypt or Delete an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - shard_count (int): Number of shards this stream will use. - action (str): The action to perform. - valid actions == create and delete - default=create - encryption_type (str): NONE or KMS - key_id (str): The GUID or alias for the KMS key - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> shard_count = 20 - >>> stream_action(client, stream_name, shard_count, action='create', encryption_type='KMS',key_id='alias/aws') - - Returns: - List (bool, str) - """ - success = False - err_msg = '' - params = { - 'StreamName': stream_name - } - try: - if not check_mode: - if action == 'start_encryption': - params['EncryptionType'] = encryption_type - params['KeyId'] = key_id - client.start_stream_encryption(**params) - success = True - elif action == 'stop_encryption': - params['EncryptionType'] = encryption_type - params['KeyId'] = key_id - client.stop_stream_encryption(**params) - success = True - else: - err_msg = 'Invalid encryption action {0}'.format(action) - else: - if action == 'start_encryption': - success = True - elif action == 'stop_encryption': - success = True - else: - err_msg = 'Invalid encryption action {0}'.format(action) - - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg - - -def retention_action(client, stream_name, retention_period=24, - action='increase', check_mode=False): - """Increase or Decrease the retention of messages in the Kinesis stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - retention_period (int): This is how long messages will be kept before - they are discarded. This can not be less than 24 hours. - action (str): The action to perform. - valid actions == create and delete - default=create - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> retention_period = 48 - >>> retention_action(client, stream_name, retention_period, action='increase') - - Returns: - Tuple (bool, str) - """ - success = False - err_msg = '' - params = { - 'StreamName': stream_name - } - try: - if not check_mode: - if action == 'increase': - params['RetentionPeriodHours'] = retention_period - client.increase_stream_retention_period(**params) - success = True - err_msg = ( - 'Retention Period increased successfully to {0}'.format(retention_period) - ) - elif action == 'decrease': - params['RetentionPeriodHours'] = retention_period - client.decrease_stream_retention_period(**params) - success = True - err_msg = ( - 'Retention Period decreased successfully to {0}'.format(retention_period) - ) - else: - err_msg = 'Invalid action {0}'.format(action) - else: - if action == 'increase': - success = True - elif action == 'decrease': - success = True - else: - err_msg = 'Invalid action {0}'.format(action) - - except botocore.exceptions.ClientError as e: - err_msg = to_native(e) - - return success, err_msg - - -def update_shard_count(client, stream_name, number_of_shards=1, check_mode=False): - """Increase or Decrease the number of shards in the Kinesis stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - number_of_shards (int): Number of shards this stream will use. - default=1 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> number_of_shards = 3 - >>> update_shard_count(client, stream_name, number_of_shards) - - Returns: - Tuple (bool, str) - """ - success = True - err_msg = '' - params = { - 'StreamName': stream_name, - 'ScalingType': 'UNIFORM_SCALING' - } - if not check_mode: - params['TargetShardCount'] = number_of_shards - try: - client.update_shard_count(**params) - except botocore.exceptions.ClientError as e: - return False, str(e) - - return success, err_msg - - -def update(client, current_stream, stream_name, number_of_shards=1, retention_period=None, - tags=None, wait=False, wait_timeout=300, check_mode=False): - """Update an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - number_of_shards (int): Number of shards this stream will use. - default=1 - retention_period (int): This is how long messages will be kept before - they are discarded. This can not be less than 24 hours. - tags (dict): The tags you want applied. - wait (bool): Wait until Stream is ACTIVE. - default=False - wait_timeout (int): How long to wait until this operation is considered failed. - default=300 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> current_stream = { - 'ShardCount': 3, - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test-stream', - 'StreamARN': 'arn:aws:kinesis:us-west-2:123456789:stream/test-stream', - 'StreamStatus': "ACTIVE' - } - >>> stream_name = 'test-stream' - >>> retention_period = 48 - >>> number_of_shards = 10 - >>> update(client, current_stream, stream_name, - number_of_shards, retention_period ) - - Returns: - Tuple (bool, bool, str) - """ - success = True - changed = False - err_msg = '' - if retention_period: - if wait: - wait_success, wait_msg, current_stream = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - if not wait_success: - return wait_success, False, wait_msg - - if current_stream.get('StreamStatus') == 'ACTIVE': - retention_changed = False - if retention_period > current_stream['RetentionPeriodHours']: - retention_changed, retention_msg = ( - retention_action( - client, stream_name, retention_period, action='increase', - check_mode=check_mode - ) - ) - - elif retention_period < current_stream['RetentionPeriodHours']: - retention_changed, retention_msg = ( - retention_action( - client, stream_name, retention_period, action='decrease', - check_mode=check_mode - ) - ) - - elif retention_period == current_stream['RetentionPeriodHours']: - retention_msg = ( - 'Retention {0} is the same as {1}' - .format( - retention_period, - current_stream['RetentionPeriodHours'] - ) - ) - success = True - - if retention_changed: - success = True - changed = True - - err_msg = retention_msg - if changed and wait: - wait_success, wait_msg, current_stream = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - if not wait_success: - return wait_success, False, wait_msg - elif changed and not wait: - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if stream_found: - if current_stream['StreamStatus'] != 'ACTIVE': - err_msg = ( - 'Retention Period for {0} is in the process of updating' - .format(stream_name) - ) - return success, changed, err_msg - else: - err_msg = ( - 'StreamStatus has to be ACTIVE in order to modify the retention period. Current status is {0}' - .format(current_stream.get('StreamStatus', 'UNKNOWN')) - ) - return success, changed, err_msg - - if current_stream['OpenShardsCount'] != number_of_shards: - success, err_msg = ( - update_shard_count(client, stream_name, number_of_shards, check_mode=check_mode) - ) - - if not success: - return success, changed, err_msg - - changed = True - - if wait: - wait_success, wait_msg, current_stream = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - if not wait_success: - return wait_success, changed, wait_msg - else: - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if stream_found and current_stream['StreamStatus'] != 'ACTIVE': - err_msg = ( - 'Number of shards for {0} is in the process of updating' - .format(stream_name) - ) - return success, changed, err_msg - - if tags: - tag_success, tag_changed, err_msg = ( - update_tags(client, stream_name, tags, check_mode=check_mode) - ) - if wait: - success, err_msg, status_stream = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - if success and changed: - err_msg = 'Kinesis Stream {0} updated successfully.'.format(stream_name) - elif success and not changed: - err_msg = 'Kinesis Stream {0} did not change.'.format(stream_name) - - return success, changed, err_msg - - -def create_stream(client, stream_name, number_of_shards=1, retention_period=None, - tags=None, wait=False, wait_timeout=300, check_mode=False): - """Create an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - number_of_shards (int): Number of shards this stream will use. - default=1 - retention_period (int): Can not be less than 24 hours - default=None - tags (dict): The tags you want applied. - default=None - wait (bool): Wait until Stream is ACTIVE. - default=False - wait_timeout (int): How long to wait until this operation is considered failed. - default=300 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> number_of_shards = 10 - >>> tags = {'env': 'test'} - >>> create_stream(client, stream_name, number_of_shards, tags=tags) - - Returns: - Tuple (bool, bool, str, dict) - """ - success = False - changed = False - err_msg = '' - results = dict() - - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - - if stream_found and current_stream.get('StreamStatus') == 'DELETING' and wait: - wait_success, wait_msg, current_stream = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - - if stream_found and current_stream.get('StreamStatus') != 'DELETING': - success, changed, err_msg = update( - client, current_stream, stream_name, number_of_shards, - retention_period, tags, wait, wait_timeout, check_mode=check_mode - ) - else: - create_success, create_msg = ( - stream_action( - client, stream_name, number_of_shards, action='create', - check_mode=check_mode - ) - ) - if not create_success: - changed = True - err_msg = 'Failed to create Kinesis stream: {0}'.format(create_msg) - return False, True, err_msg, {} - else: - changed = True - if wait: - wait_success, wait_msg, results = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - err_msg = ( - 'Kinesis Stream {0} is in the process of being created' - .format(stream_name) - ) - if not wait_success: - return wait_success, True, wait_msg, results - else: - err_msg = ( - 'Kinesis Stream {0} created successfully' - .format(stream_name) - ) - - if tags: - changed, err_msg = ( - tags_action( - client, stream_name, tags, action='create', - check_mode=check_mode - ) - ) - if changed: - success = True - if not success: - return success, changed, err_msg, results - - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if retention_period and current_stream.get('StreamStatus') == 'ACTIVE': - changed, err_msg = ( - retention_action( - client, stream_name, retention_period, action='increase', - check_mode=check_mode - ) - ) - if changed: - success = True - if not success: - return success, changed, err_msg, results - else: - err_msg = ( - 'StreamStatus has to be ACTIVE in order to modify the retention period. Current status is {0}' - .format(current_stream.get('StreamStatus', 'UNKNOWN')) - ) - success = create_success - changed = True - - if success: - stream_found, stream_msg, results = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - tag_success, tag_msg, current_tags = ( - get_tags(client, stream_name, check_mode=check_mode) - ) - if current_tags and not check_mode: - current_tags = make_tags_in_proper_format(current_tags) - results['Tags'] = current_tags - elif check_mode and tags: - results['Tags'] = tags - else: - results['Tags'] = dict() - results = convert_to_lower(results) - - return success, changed, err_msg, results - - -def delete_stream(client, stream_name, wait=False, wait_timeout=300, - check_mode=False): - """Delete an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - wait (bool): Wait until Stream is ACTIVE. - default=False - wait_timeout (int): How long to wait until this operation is considered failed. - default=300 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> delete_stream(client, stream_name) - - Returns: - Tuple (bool, bool, str, dict) - """ - success = False - changed = False - err_msg = '' - results = dict() - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if stream_found: - success, err_msg = ( - stream_action( - client, stream_name, action='delete', check_mode=check_mode - ) - ) - if success: - changed = True - if wait: - success, err_msg, results = ( - wait_for_status( - client, stream_name, 'DELETING', wait_timeout, - check_mode=check_mode - ) - ) - err_msg = 'Stream {0} deleted successfully'.format(stream_name) - if not success: - return success, True, err_msg, results - else: - err_msg = ( - 'Stream {0} is in the process of being deleted' - .format(stream_name) - ) - else: - success = True - changed = False - err_msg = 'Stream {0} does not exist'.format(stream_name) - - return success, changed, err_msg, results - - -def start_stream_encryption(client, stream_name, encryption_type='', key_id='', - wait=False, wait_timeout=300, check_mode=False): - """Start encryption on an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - encryption_type (str): KMS or NONE - key_id (str): KMS key GUID or alias - wait (bool): Wait until Stream is ACTIVE. - default=False - wait_timeout (int): How long to wait until this operation is considered failed. - default=300 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> key_id = 'alias/aws' - >>> encryption_type = 'KMS' - >>> start_stream_encryption(client, stream_name,encryption_type,key_id) - - Returns: - Tuple (bool, bool, str, dict) - """ - success = False - changed = False - err_msg = '' - params = { - 'StreamName': stream_name - } - - results = dict() - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if stream_found: - success, err_msg = ( - stream_encryption_action( - client, stream_name, action='start_encryption', encryption_type=encryption_type, key_id=key_id, check_mode=check_mode - ) - ) - if success: - changed = True - if wait: - success, err_msg, results = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - err_msg = 'Kinesis Stream {0} encryption started successfully.'.format(stream_name) - if not success: - return success, True, err_msg, results - else: - err_msg = ( - 'Kinesis Stream {0} is in the process of starting encryption.'.format(stream_name) - ) - else: - success = True - changed = False - err_msg = 'Kinesis Stream {0} does not exist'.format(stream_name) - - return success, changed, err_msg, results - - -def stop_stream_encryption(client, stream_name, encryption_type='', key_id='', - wait=True, wait_timeout=300, check_mode=False): - """Stop encryption on an Amazon Kinesis Stream. - Args: - client (botocore.client.EC2): Boto3 client. - stream_name (str): The name of the kinesis stream. - - Kwargs: - encryption_type (str): KMS or NONE - key_id (str): KMS key GUID or alias - wait (bool): Wait until Stream is ACTIVE. - default=False - wait_timeout (int): How long to wait until this operation is considered failed. - default=300 - check_mode (bool): This will pass DryRun as one of the parameters to the aws api. - default=False - - Basic Usage: - >>> client = boto3.client('kinesis') - >>> stream_name = 'test-stream' - >>> start_stream_encryption(client, stream_name,encryption_type, key_id) - - Returns: - Tuple (bool, bool, str, dict) - """ - success = False - changed = False - err_msg = '' - params = { - 'StreamName': stream_name - } - - results = dict() - stream_found, stream_msg, current_stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) - if stream_found: - if current_stream.get('EncryptionType') == 'KMS': - success, err_msg = ( - stream_encryption_action( - client, stream_name, action='stop_encryption', key_id=key_id, encryption_type=encryption_type, check_mode=check_mode - ) - ) - elif current_stream.get('EncryptionType') == 'NONE': - success = True - - if success: - changed = True - if wait: - success, err_msg, results = ( - wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, - check_mode=check_mode - ) - ) - err_msg = 'Kinesis Stream {0} encryption stopped successfully.'.format(stream_name) - if not success: - return success, True, err_msg, results - else: - err_msg = ( - 'Stream {0} is in the process of stopping encryption.'.format(stream_name) - ) - else: - success = True - changed = False - err_msg = 'Stream {0} does not exist.'.format(stream_name) - - return success, changed, err_msg, results - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True), - shards=dict(default=None, required=False, type='int'), - retention_period=dict(default=None, required=False, type='int'), - tags=dict(default=None, required=False, type='dict', aliases=['resource_tags']), - wait=dict(default=True, required=False, type='bool'), - wait_timeout=dict(default=300, required=False, type='int'), - state=dict(default='present', choices=['present', 'absent']), - encryption_type=dict(required=False, choices=['NONE', 'KMS']), - key_id=dict(required=False, type='str'), - encryption_state=dict(required=False, choices=['enabled', 'disabled']), - ) - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - retention_period = module.params.get('retention_period') - stream_name = module.params.get('name') - shards = module.params.get('shards') - state = module.params.get('state') - tags = module.params.get('tags') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - encryption_type = module.params.get('encryption_type') - key_id = module.params.get('key_id') - encryption_state = module.params.get('encryption_state') - - if state == 'present' and not shards: - module.fail_json(msg='Shards is required when state == present.') - - if retention_period: - if retention_period < 24: - module.fail_json(msg='Retention period can not be less than 24 hours.') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - check_mode = module.check_mode - try: - region, ec2_url, aws_connect_kwargs = ( - get_aws_connection_info(module, boto3=True) - ) - client = ( - boto3_conn( - module, conn_type='client', resource='kinesis', - region=region, endpoint=ec2_url, **aws_connect_kwargs - ) - ) - except botocore.exceptions.ClientError as e: - err_msg = 'Boto3 Client Error - {0}'.format(to_native(e.msg)) - module.fail_json( - success=False, changed=False, result={}, msg=err_msg - ) - - if state == 'present': - success, changed, err_msg, results = ( - create_stream( - client, stream_name, shards, retention_period, tags, - wait, wait_timeout, check_mode - ) - ) - if encryption_state == 'enabled': - success, changed, err_msg, results = ( - start_stream_encryption( - client, stream_name, encryption_type, key_id, wait, wait_timeout, check_mode - ) - ) - elif encryption_state == 'disabled': - success, changed, err_msg, results = ( - stop_stream_encryption( - client, stream_name, encryption_type, key_id, wait, wait_timeout, check_mode - ) - ) - elif state == 'absent': - success, changed, err_msg, results = ( - delete_stream(client, stream_name, wait, wait_timeout, check_mode) - ) - - if success: - module.exit_json( - success=success, changed=changed, msg=err_msg, **results - ) - else: - module.fail_json( - success=success, changed=changed, msg=err_msg, result=results - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lambda.py b/lib/ansible/modules/cloud/amazon/lambda.py deleted file mode 100644 index 800d81cc15..0000000000 --- a/lib/ansible/modules/cloud/amazon/lambda.py +++ /dev/null @@ -1,628 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: lambda -short_description: Manage AWS Lambda functions -description: - - Allows for the management of Lambda functions. -version_added: '2.2' -requirements: [ boto3 ] -options: - name: - description: - - The name you want to assign to the function you are uploading. Cannot be changed. - required: true - type: str - state: - description: - - Create or delete Lambda function. - default: present - choices: [ 'present', 'absent' ] - type: str - runtime: - description: - - The runtime environment for the Lambda function you are uploading. - - Required when creating a function. Uses parameters as described in boto3 docs. - - Required when I(state=present). - - For supported list of runtimes, see U(https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). - type: str - role: - description: - - The Amazon Resource Name (ARN) of the IAM role that Lambda assumes when it executes your function to access any other Amazon Web Services (AWS) - resources. You may use the bare ARN if the role belongs to the same AWS account. - - Required when I(state=present). - type: str - handler: - description: - - The function within your code that Lambda calls to begin execution. - type: str - zip_file: - description: - - A .zip file containing your deployment package - - If I(state=present) then either I(zip_file) or I(s3_bucket) must be present. - aliases: [ 'src' ] - type: str - s3_bucket: - description: - - Amazon S3 bucket name where the .zip file containing your deployment package is stored. - - If I(state=present) then either I(zip_file) or I(s3_bucket) must be present. - - I(s3_bucket) and I(s3_key) are required together. - type: str - s3_key: - description: - - The Amazon S3 object (the deployment package) key name you want to upload. - - I(s3_bucket) and I(s3_key) are required together. - type: str - s3_object_version: - description: - - The Amazon S3 object (the deployment package) version you want to upload. - type: str - description: - description: - - A short, user-defined function description. Lambda does not use this value. Assign a meaningful description as you see fit. - type: str - timeout: - description: - - The function maximum execution time in seconds after which Lambda should terminate the function. - default: 3 - type: int - memory_size: - description: - - The amount of memory, in MB, your Lambda function is given. - default: 128 - type: int - vpc_subnet_ids: - description: - - List of subnet IDs to run Lambda function in. - - Use this option if you need to access resources in your VPC. Leave empty if you don't want to run the function in a VPC. - - If set, I(vpc_security_group_ids) must also be set. - type: list - elements: str - vpc_security_group_ids: - description: - - List of VPC security group IDs to associate with the Lambda function. - - Required when I(vpc_subnet_ids) is used. - type: list - elements: str - environment_variables: - description: - - A dictionary of environment variables the Lambda function is given. - version_added: "2.3" - type: dict - dead_letter_arn: - description: - - The parent object that contains the target Amazon Resource Name (ARN) of an Amazon SQS queue or Amazon SNS topic. - version_added: "2.3" - type: str - tracing_mode: - description: - - Set mode to 'Active' to sample and trace incoming requests with AWS X-Ray. Turned off (set to 'PassThrough') by default. - choices: ['Active', 'PassThrough'] - version_added: "2.10" - type: str - tags: - description: - - tag dict to apply to the function (requires botocore 1.5.40 or above). - version_added: "2.5" - type: dict -author: - - 'Steyn Huizinga (@steynovich)' -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create Lambda functions -- name: looped creation - lambda: - name: '{{ item.name }}' - state: present - zip_file: '{{ item.zip_file }}' - runtime: 'python2.7' - role: 'arn:aws:iam::987654321012:role/lambda_basic_execution' - handler: 'hello_python.my_handler' - vpc_subnet_ids: - - subnet-123abcde - - subnet-edcba321 - vpc_security_group_ids: - - sg-123abcde - - sg-edcba321 - environment_variables: '{{ item.env_vars }}' - tags: - key1: 'value1' - loop: - - name: HelloWorld - zip_file: hello-code.zip - env_vars: - key1: "first" - key2: "second" - - name: ByeBye - zip_file: bye-code.zip - env_vars: - key1: "1" - key2: "2" - -# To remove previously added tags pass an empty dict -- name: remove tags - lambda: - name: 'Lambda function' - state: present - zip_file: 'code.zip' - runtime: 'python2.7' - role: 'arn:aws:iam::987654321012:role/lambda_basic_execution' - handler: 'hello_python.my_handler' - tags: {} - -# Basic Lambda function deletion -- name: Delete Lambda functions HelloWorld and ByeBye - lambda: - name: '{{ item }}' - state: absent - loop: - - HelloWorld - - ByeBye -''' - -RETURN = ''' -code: - description: the lambda function location returned by get_function in boto3 - returned: success - type: dict - sample: - { - 'location': 'a presigned S3 URL', - 'repository_type': 'S3', - } -configuration: - description: the lambda function metadata returned by get_function in boto3 - returned: success - type: dict - sample: - { - 'code_sha256': 'zOAGfF5JLFuzZoSNirUtOrQp+S341IOA3BcoXXoaIaU=', - 'code_size': 123, - 'description': 'My function', - 'environment': { - 'variables': { - 'key': 'value' - } - }, - 'function_arn': 'arn:aws:lambda:us-east-1:123456789012:function:myFunction:1', - 'function_name': 'myFunction', - 'handler': 'index.handler', - 'last_modified': '2017-08-01T00:00:00.000+0000', - 'memory_size': 128, - 'revision_id': 'a2x9886d-d48a-4a0c-ab64-82abc005x80c', - 'role': 'arn:aws:iam::123456789012:role/lambda_basic_execution', - 'runtime': 'nodejs6.10', - 'tracing_config': { 'mode': 'Active' }, - 'timeout': 3, - 'version': '1', - 'vpc_config': { - 'security_group_ids': [], - 'subnet_ids': [], - 'vpc_id': '123' - } - } -''' - -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import compare_aws_tags -import base64 -import hashlib -import traceback -import re - -try: - from botocore.exceptions import ClientError, BotoCoreError, ValidationError, ParamValidationError -except ImportError: - pass # protected by AnsibleAWSModule - - -def get_account_info(module, region=None, endpoint=None, **aws_connect_kwargs): - """return the account information (account id and partition) we are currently working on - - get_account_info tries too find out the account that we are working - on. It's not guaranteed that this will be easy so we try in - several different ways. Giving either IAM or STS privileges to - the account should be enough to permit this. - """ - account_id = None - partition = None - try: - sts_client = boto3_conn(module, conn_type='client', resource='sts', - region=region, endpoint=endpoint, **aws_connect_kwargs) - caller_id = sts_client.get_caller_identity() - account_id = caller_id.get('Account') - partition = caller_id.get('Arn').split(':')[1] - except ClientError: - try: - iam_client = boto3_conn(module, conn_type='client', resource='iam', - region=region, endpoint=endpoint, **aws_connect_kwargs) - arn, partition, service, reg, account_id, resource = iam_client.get_user()['User']['Arn'].split(':') - except ClientError as e: - if (e.response['Error']['Code'] == 'AccessDenied'): - except_msg = to_native(e.message) - m = except_msg.search(r"arn:(aws(-([a-z\-]+))?):iam::([0-9]{12,32}):\w+/") - account_id = m.group(4) - partition = m.group(1) - if account_id is None: - module.fail_json_aws(e, msg="getting account information") - if partition is None: - module.fail_json_aws(e, msg="getting account information: partition") - except Exception as e: - module.fail_json_aws(e, msg="getting account information") - - return account_id, partition - - -def get_current_function(connection, function_name, qualifier=None): - try: - if qualifier is not None: - return connection.get_function(FunctionName=function_name, Qualifier=qualifier) - return connection.get_function(FunctionName=function_name) - except ClientError as e: - try: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - return None - except (KeyError, AttributeError): - pass - raise e - - -def sha256sum(filename): - hasher = hashlib.sha256() - with open(filename, 'rb') as f: - hasher.update(f.read()) - - code_hash = hasher.digest() - code_b64 = base64.b64encode(code_hash) - hex_digest = code_b64.decode('utf-8') - - return hex_digest - - -def set_tag(client, module, tags, function): - if not hasattr(client, "list_tags"): - module.fail_json(msg="Using tags requires botocore 1.5.40 or above") - - changed = False - arn = function['Configuration']['FunctionArn'] - - try: - current_tags = client.list_tags(Resource=arn).get('Tags', {}) - except ClientError as e: - module.fail_json(msg="Unable to list tags: {0}".format(to_native(e)), - exception=traceback.format_exc()) - - tags_to_add, tags_to_remove = compare_aws_tags(current_tags, tags, purge_tags=True) - - try: - if tags_to_remove: - client.untag_resource( - Resource=arn, - TagKeys=tags_to_remove - ) - changed = True - - if tags_to_add: - client.tag_resource( - Resource=arn, - Tags=tags_to_add - ) - changed = True - - except ClientError as e: - module.fail_json(msg="Unable to tag resource {0}: {1}".format(arn, - to_native(e)), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - except BotoCoreError as e: - module.fail_json(msg="Unable to tag resource {0}: {1}".format(arn, - to_native(e)), exception=traceback.format_exc()) - - return changed - - -def main(): - argument_spec = dict( - name=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - runtime=dict(), - role=dict(), - handler=dict(), - zip_file=dict(aliases=['src']), - s3_bucket=dict(), - s3_key=dict(), - s3_object_version=dict(), - description=dict(default=''), - timeout=dict(type='int', default=3), - memory_size=dict(type='int', default=128), - vpc_subnet_ids=dict(type='list'), - vpc_security_group_ids=dict(type='list'), - environment_variables=dict(type='dict'), - dead_letter_arn=dict(), - tracing_mode=dict(choices=['Active', 'PassThrough']), - tags=dict(type='dict'), - ) - - mutually_exclusive = [['zip_file', 's3_key'], - ['zip_file', 's3_bucket'], - ['zip_file', 's3_object_version']] - - required_together = [['s3_key', 's3_bucket'], - ['vpc_subnet_ids', 'vpc_security_group_ids']] - - required_if = [['state', 'present', ['runtime', 'handler', 'role']]] - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=mutually_exclusive, - required_together=required_together, - required_if=required_if) - - name = module.params.get('name') - state = module.params.get('state').lower() - runtime = module.params.get('runtime') - role = module.params.get('role') - handler = module.params.get('handler') - s3_bucket = module.params.get('s3_bucket') - s3_key = module.params.get('s3_key') - s3_object_version = module.params.get('s3_object_version') - zip_file = module.params.get('zip_file') - description = module.params.get('description') - timeout = module.params.get('timeout') - memory_size = module.params.get('memory_size') - vpc_subnet_ids = module.params.get('vpc_subnet_ids') - vpc_security_group_ids = module.params.get('vpc_security_group_ids') - environment_variables = module.params.get('environment_variables') - dead_letter_arn = module.params.get('dead_letter_arn') - tracing_mode = module.params.get('tracing_mode') - tags = module.params.get('tags') - - check_mode = module.check_mode - changed = False - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg='region must be specified') - - try: - client = boto3_conn(module, conn_type='client', resource='lambda', - region=region, endpoint=ec2_url, **aws_connect_kwargs) - except (ClientError, ValidationError) as e: - module.fail_json_aws(e, msg="Trying to connect to AWS") - - if state == 'present': - if re.match(r'^arn:aws(-([a-z\-]+))?:iam', role): - role_arn = role - else: - # get account ID and assemble ARN - account_id, partition = get_account_info(module, region=region, endpoint=ec2_url, **aws_connect_kwargs) - role_arn = 'arn:{0}:iam::{1}:role/{2}'.format(partition, account_id, role) - - # Get function configuration if present, False otherwise - current_function = get_current_function(client, name) - - # Update existing Lambda function - if state == 'present' and current_function: - - # Get current state - current_config = current_function['Configuration'] - current_version = None - - # Update function configuration - func_kwargs = {'FunctionName': name} - - # Update configuration if needed - if role_arn and current_config['Role'] != role_arn: - func_kwargs.update({'Role': role_arn}) - if handler and current_config['Handler'] != handler: - func_kwargs.update({'Handler': handler}) - if description and current_config['Description'] != description: - func_kwargs.update({'Description': description}) - if timeout and current_config['Timeout'] != timeout: - func_kwargs.update({'Timeout': timeout}) - if memory_size and current_config['MemorySize'] != memory_size: - func_kwargs.update({'MemorySize': memory_size}) - if runtime and current_config['Runtime'] != runtime: - func_kwargs.update({'Runtime': runtime}) - if (environment_variables is not None) and (current_config.get( - 'Environment', {}).get('Variables', {}) != environment_variables): - func_kwargs.update({'Environment': {'Variables': environment_variables}}) - if dead_letter_arn is not None: - if current_config.get('DeadLetterConfig'): - if current_config['DeadLetterConfig']['TargetArn'] != dead_letter_arn: - func_kwargs.update({'DeadLetterConfig': {'TargetArn': dead_letter_arn}}) - else: - if dead_letter_arn != "": - func_kwargs.update({'DeadLetterConfig': {'TargetArn': dead_letter_arn}}) - if tracing_mode and (current_config.get('TracingConfig', {}).get('Mode', 'PassThrough') != tracing_mode): - func_kwargs.update({'TracingConfig': {'Mode': tracing_mode}}) - - # If VPC configuration is desired - if vpc_subnet_ids or vpc_security_group_ids: - if not vpc_subnet_ids or not vpc_security_group_ids: - module.fail_json(msg='vpc connectivity requires at least one security group and one subnet') - - if 'VpcConfig' in current_config: - # Compare VPC config with current config - current_vpc_subnet_ids = current_config['VpcConfig']['SubnetIds'] - current_vpc_security_group_ids = current_config['VpcConfig']['SecurityGroupIds'] - - subnet_net_id_changed = sorted(vpc_subnet_ids) != sorted(current_vpc_subnet_ids) - vpc_security_group_ids_changed = sorted(vpc_security_group_ids) != sorted(current_vpc_security_group_ids) - - if 'VpcConfig' not in current_config or subnet_net_id_changed or vpc_security_group_ids_changed: - new_vpc_config = {'SubnetIds': vpc_subnet_ids, - 'SecurityGroupIds': vpc_security_group_ids} - func_kwargs.update({'VpcConfig': new_vpc_config}) - else: - # No VPC configuration is desired, assure VPC config is empty when present in current config - if 'VpcConfig' in current_config and current_config['VpcConfig'].get('VpcId'): - func_kwargs.update({'VpcConfig': {'SubnetIds': [], 'SecurityGroupIds': []}}) - - # Upload new configuration if configuration has changed - if len(func_kwargs) > 1: - try: - if not check_mode: - response = client.update_function_configuration(**func_kwargs) - current_version = response['Version'] - changed = True - except (ParamValidationError, ClientError) as e: - module.fail_json_aws(e, msg="Trying to update lambda configuration") - - # Update code configuration - code_kwargs = {'FunctionName': name, 'Publish': True} - - # Update S3 location - if s3_bucket and s3_key: - # If function is stored on S3 always update - code_kwargs.update({'S3Bucket': s3_bucket, 'S3Key': s3_key}) - - # If S3 Object Version is given - if s3_object_version: - code_kwargs.update({'S3ObjectVersion': s3_object_version}) - - # Compare local checksum, update remote code when different - elif zip_file: - local_checksum = sha256sum(zip_file) - remote_checksum = current_config['CodeSha256'] - - # Only upload new code when local code is different compared to the remote code - if local_checksum != remote_checksum: - try: - with open(zip_file, 'rb') as f: - encoded_zip = f.read() - code_kwargs.update({'ZipFile': encoded_zip}) - except IOError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc()) - - # Tag Function - if tags is not None: - if set_tag(client, module, tags, current_function): - changed = True - - # Upload new code if needed (e.g. code checksum has changed) - if len(code_kwargs) > 2: - try: - if not check_mode: - response = client.update_function_code(**code_kwargs) - current_version = response['Version'] - changed = True - except (ParamValidationError, ClientError) as e: - module.fail_json_aws(e, msg="Trying to upload new code") - - # Describe function code and configuration - response = get_current_function(client, name, qualifier=current_version) - if not response: - module.fail_json(msg='Unable to get function information after updating') - - # We're done - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - # Function doesn't exists, create new Lambda function - elif state == 'present': - if s3_bucket and s3_key: - # If function is stored on S3 - code = {'S3Bucket': s3_bucket, - 'S3Key': s3_key} - if s3_object_version: - code.update({'S3ObjectVersion': s3_object_version}) - elif zip_file: - # If function is stored in local zipfile - try: - with open(zip_file, 'rb') as f: - zip_content = f.read() - - code = {'ZipFile': zip_content} - except IOError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc()) - - else: - module.fail_json(msg='Either S3 object or path to zipfile required') - - func_kwargs = {'FunctionName': name, - 'Publish': True, - 'Runtime': runtime, - 'Role': role_arn, - 'Code': code, - 'Timeout': timeout, - 'MemorySize': memory_size, - } - - if description is not None: - func_kwargs.update({'Description': description}) - - if handler is not None: - func_kwargs.update({'Handler': handler}) - - if environment_variables: - func_kwargs.update({'Environment': {'Variables': environment_variables}}) - - if dead_letter_arn: - func_kwargs.update({'DeadLetterConfig': {'TargetArn': dead_letter_arn}}) - - if tracing_mode: - func_kwargs.update({'TracingConfig': {'Mode': tracing_mode}}) - - # If VPC configuration is given - if vpc_subnet_ids or vpc_security_group_ids: - if not vpc_subnet_ids or not vpc_security_group_ids: - module.fail_json(msg='vpc connectivity requires at least one security group and one subnet') - - func_kwargs.update({'VpcConfig': {'SubnetIds': vpc_subnet_ids, - 'SecurityGroupIds': vpc_security_group_ids}}) - - # Finally try to create function - current_version = None - try: - if not check_mode: - response = client.create_function(**func_kwargs) - current_version = response['Version'] - changed = True - except (ParamValidationError, ClientError) as e: - module.fail_json_aws(e, msg="Trying to create function") - - # Tag Function - if tags is not None: - if set_tag(client, module, tags, get_current_function(client, name)): - changed = True - - response = get_current_function(client, name, qualifier=current_version) - if not response: - module.fail_json(msg='Unable to get function information after creating') - module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - - # Delete existing Lambda function - if state == 'absent' and current_function: - try: - if not check_mode: - client.delete_function(FunctionName=name) - changed = True - except (ParamValidationError, ClientError) as e: - module.fail_json_aws(e, msg="Trying to delete Lambda function") - - module.exit_json(changed=changed) - - # Function already absent, do nothing - elif state == 'absent': - module.exit_json(changed=changed) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lambda_alias.py b/lib/ansible/modules/cloud/amazon/lambda_alias.py deleted file mode 100644 index 54bd3b6e79..0000000000 --- a/lib/ansible/modules/cloud/amazon/lambda_alias.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: lambda_alias -short_description: Creates, updates or deletes AWS Lambda function aliases -description: - - This module allows the management of AWS Lambda functions aliases via the Ansible - framework. It is idempotent and supports "Check" mode. Use module M(lambda) to manage the lambda function - itself and M(lambda_event) to manage event source mappings. - -version_added: "2.2" - -author: Pierre Jodouin (@pjodouin), Ryan Scott Brown (@ryansb) -options: - function_name: - description: - - The name of the function alias. - required: true - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - name: - description: - - Name of the function alias. - required: true - aliases: ['alias_name'] - type: str - description: - description: - - A short, user-defined function alias description. - type: str - function_version: - description: - - Version associated with the Lambda function alias. - A value of 0 (or omitted parameter) sets the alias to the $LATEST version. - aliases: ['version'] - type: int -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -# Simple example to create a lambda function and publish a version -- hosts: localhost - gather_facts: no - vars: - state: present - project_folder: /path/to/deployment/package - deployment_package: lambda.zip - account: 123456789012 - production_version: 5 - tasks: - - name: AWS Lambda Function - lambda: - state: "{{ state | default('present') }}" - name: myLambdaFunction - publish: True - description: lambda function description - code_s3_bucket: package-bucket - code_s3_key: "lambda/{{ deployment_package }}" - local_path: "{{ project_folder }}/{{ deployment_package }}" - runtime: python2.7 - timeout: 5 - handler: lambda.handler - memory_size: 128 - role: "arn:aws:iam::{{ account }}:role/API2LambdaExecRole" - - - name: Get information - lambda_info: - name: myLambdaFunction - register: lambda_info - - name: show results - debug: - msg: "{{ lambda_info['lambda_facts'] }}" - -# The following will set the Dev alias to the latest version ($LATEST) since version is omitted (or = 0) - - name: "alias 'Dev' for function {{ lambda_info.lambda_facts.FunctionName }} " - lambda_alias: - state: "{{ state | default('present') }}" - function_name: "{{ lambda_info.lambda_facts.FunctionName }}" - name: Dev - description: Development is $LATEST version - -# The QA alias will only be created when a new version is published (i.e. not = '$LATEST') - - name: "alias 'QA' for function {{ lambda_info.lambda_facts.FunctionName }} " - lambda_alias: - state: "{{ state | default('present') }}" - function_name: "{{ lambda_info.lambda_facts.FunctionName }}" - name: QA - version: "{{ lambda_info.lambda_facts.Version }}" - description: "QA is version {{ lambda_info.lambda_facts.Version }}" - when: lambda_info.lambda_facts.Version != "$LATEST" - -# The Prod alias will have a fixed version based on a variable - - name: "alias 'Prod' for function {{ lambda_info.lambda_facts.FunctionName }} " - lambda_alias: - state: "{{ state | default('present') }}" - function_name: "{{ lambda_info.lambda_facts.FunctionName }}" - name: Prod - version: "{{ production_version }}" - description: "Production is version {{ production_version }}" -''' - -RETURN = ''' ---- -alias_arn: - description: Full ARN of the function, including the alias - returned: success - type: str - sample: arn:aws:lambda:us-west-2:123456789012:function:myFunction:dev -description: - description: A short description of the alias - returned: success - type: str - sample: The development stage for my hot new app -function_version: - description: The qualifier that the alias refers to - returned: success - type: str - sample: $LATEST -name: - description: The name of the alias assigned - returned: success - type: str - sample: dev -''' - -import re - -try: - import boto3 - from botocore.exceptions import ClientError, ParamValidationError, MissingParametersError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info) - - -class AWSConnection: - """ - Create the connection object and client objects as required. - """ - - def __init__(self, ansible_obj, resources, boto3_=True): - - try: - self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=boto3_) - - self.resource_client = dict() - if not resources: - resources = ['lambda'] - - resources.append('iam') - - for resource in resources: - aws_connect_kwargs.update(dict(region=self.region, - endpoint=self.endpoint, - conn_type='client', - resource=resource - )) - self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs) - - # if region is not provided, then get default profile/session region - if not self.region: - self.region = self.resource_client['lambda'].meta.region_name - - except (ClientError, ParamValidationError, MissingParametersError) as e: - ansible_obj.fail_json(msg="Unable to connect, authorize or access resource: {0}".format(e)) - - try: - self.account_id = self.resource_client['iam'].get_user()['User']['Arn'].split(':')[4] - except (ClientError, ValueError, KeyError, IndexError): - self.account_id = '' - - def client(self, resource='lambda'): - return self.resource_client[resource] - - -def pc(key): - """ - Changes python key into Pascale case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'. - - :param key: - :return: - """ - - return "".join([token.capitalize() for token in key.split('_')]) - - -def set_api_params(module, module_params): - """ - Sets module parameters to those expected by the boto3 API. - - :param module: - :param module_params: - :return: - """ - - api_params = dict() - - for param in module_params: - module_param = module.params.get(param, None) - if module_param: - api_params[pc(param)] = module_param - - return api_params - - -def validate_params(module, aws): - """ - Performs basic parameter validation. - - :param module: Ansible module reference - :param aws: AWS client connection - :return: - """ - - function_name = module.params['function_name'] - - # validate function name - if not re.search(r'^[\w\-:]+$', function_name): - module.fail_json( - msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) - ) - if len(function_name) > 64: - module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - - # if parameter 'function_version' is zero, set it to $LATEST, else convert it to a string - if module.params['function_version'] == 0: - module.params['function_version'] = '$LATEST' - else: - module.params['function_version'] = str(module.params['function_version']) - - return - - -def get_lambda_alias(module, aws): - """ - Returns the lambda function alias if it exists. - - :param module: Ansible module reference - :param aws: AWS client connection - :return: - """ - - client = aws.client('lambda') - - # set API parameters - api_params = set_api_params(module, ('function_name', 'name')) - - # check if alias exists and get facts - try: - results = client.get_alias(**api_params) - - except (ClientError, ParamValidationError, MissingParametersError) as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - results = None - else: - module.fail_json(msg='Error retrieving function alias: {0}'.format(e)) - - return results - - -def lambda_alias(module, aws): - """ - Adds, updates or deletes lambda function aliases. - - :param module: Ansible module reference - :param aws: AWS client connection - :return dict: - """ - client = aws.client('lambda') - results = dict() - changed = False - current_state = 'absent' - state = module.params['state'] - - facts = get_lambda_alias(module, aws) - if facts: - current_state = 'present' - - if state == 'present': - if current_state == 'present': - - # check if alias has changed -- only version and description can change - alias_params = ('function_version', 'description') - for param in alias_params: - if module.params.get(param) != facts.get(pc(param)): - changed = True - break - - if changed: - api_params = set_api_params(module, ('function_name', 'name')) - api_params.update(set_api_params(module, alias_params)) - - if not module.check_mode: - try: - results = client.update_alias(**api_params) - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error updating function alias: {0}'.format(e)) - - else: - # create new function alias - api_params = set_api_params(module, ('function_name', 'name', 'function_version', 'description')) - - try: - if not module.check_mode: - results = client.create_alias(**api_params) - changed = True - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error creating function alias: {0}'.format(e)) - - else: # state = 'absent' - if current_state == 'present': - # delete the function - api_params = set_api_params(module, ('function_name', 'name')) - - try: - if not module.check_mode: - results = client.delete_alias(**api_params) - changed = True - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error deleting function alias: {0}'.format(e)) - - return dict(changed=changed, **dict(results or facts)) - - -def main(): - """ - Main entry point. - - :return dict: ansible facts - """ - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - state=dict(required=False, default='present', choices=['present', 'absent']), - function_name=dict(required=True), - name=dict(required=True, aliases=['alias_name']), - function_version=dict(type='int', required=False, default=0, aliases=['version']), - description=dict(required=False, default=None), - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[], - required_together=[] - ) - - # validate dependencies - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for this module.') - - aws = AWSConnection(module, ['lambda']) - - validate_params(module, aws) - - results = lambda_alias(module, aws) - - module.exit_json(**camel_dict_to_snake_dict(results)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lambda_event.py b/lib/ansible/modules/cloud/amazon/lambda_event.py deleted file mode 100644 index fcd3960f62..0000000000 --- a/lib/ansible/modules/cloud/amazon/lambda_event.py +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/python -# (c) 2016, Pierre Jodouin <pjodouin@virtualcomputing.solutions> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: lambda_event -short_description: Creates, updates or deletes AWS Lambda function event mappings -description: - - This module allows the management of AWS Lambda function event source mappings such as DynamoDB and Kinesis stream - events via the Ansible framework. These event source mappings are relevant only in the AWS Lambda pull model, where - AWS Lambda invokes the function. - It is idempotent and supports "Check" mode. Use module M(lambda) to manage the lambda - function itself and M(lambda_alias) to manage function aliases. - -version_added: "2.2" - -author: Pierre Jodouin (@pjodouin), Ryan Brown (@ryansb) -options: - lambda_function_arn: - description: - - The name or ARN of the lambda function. - required: true - aliases: ['function_name', 'function_arn'] - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - alias: - description: - - Name of the function alias. - - Mutually exclusive with I(version). - type: str - version: - description: - - Version of the Lambda function. - - Mutually exclusive with I(alias). - type: int - event_source: - description: - - Source of the event that triggers the lambda function. - - For DynamoDB and Kinesis events, select C(stream) - - For SQS queues, select C(sqs) - default: stream - choices: ['stream', 'sqs'] - type: str - source_params: - description: - - Sub-parameters required for event source. - suboptions: - source_arn: - description: - - The Amazon Resource Name (ARN) of the SQS queue, Kinesis stream or DynamoDB stream that is the event source. - type: str - required: true - enabled: - description: - - Indicates whether AWS Lambda should begin polling or readin from the event source. - default: true. - type: bool - batch_size: - description: - - The largest number of records that AWS Lambda will retrieve from your event source at the time of invoking your function. - default: 100 - type: int - starting_position: - description: - - The position in the stream where AWS Lambda should start reading. - - Required when I(event_source=stream). - choices: [TRIM_HORIZON,LATEST] - type: str - required: true - type: dict -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -# Example that creates a lambda event notification for a DynamoDB stream -- hosts: localhost - gather_facts: no - vars: - state: present - tasks: - - name: DynamoDB stream event mapping - lambda_event: - state: "{{ state | default('present') }}" - event_source: stream - function_name: "{{ function_name }}" - alias: Dev - source_params: - source_arn: arn:aws:dynamodb:us-east-1:123456789012:table/tableName/stream/2016-03-19T19:51:37.457 - enabled: True - batch_size: 100 - starting_position: TRIM_HORIZON - - - name: Show source event - debug: - var: lambda_stream_events -''' - -RETURN = ''' ---- -lambda_stream_events: - description: list of dictionaries returned by the API describing stream event mappings - returned: success - type: list -''' - -import re -import sys - -try: - import boto3 - from botocore.exceptions import ClientError, ParamValidationError, MissingParametersError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info) - - -# --------------------------------------------------------------------------------------------------- -# -# Helper Functions & classes -# -# --------------------------------------------------------------------------------------------------- - - -class AWSConnection: - """ - Create the connection object and client objects as required. - """ - - def __init__(self, ansible_obj, resources, use_boto3=True): - - try: - self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=use_boto3) - - self.resource_client = dict() - if not resources: - resources = ['lambda'] - - resources.append('iam') - - for resource in resources: - aws_connect_kwargs.update(dict(region=self.region, - endpoint=self.endpoint, - conn_type='client', - resource=resource - )) - self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs) - - # if region is not provided, then get default profile/session region - if not self.region: - self.region = self.resource_client['lambda'].meta.region_name - - except (ClientError, ParamValidationError, MissingParametersError) as e: - ansible_obj.fail_json(msg="Unable to connect, authorize or access resource: {0}".format(e)) - - # set account ID - try: - self.account_id = self.resource_client['iam'].get_user()['User']['Arn'].split(':')[4] - except (ClientError, ValueError, KeyError, IndexError): - self.account_id = '' - - def client(self, resource='lambda'): - return self.resource_client[resource] - - -def pc(key): - """ - Changes python key into Pascale case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'. - - :param key: - :return: - """ - - return "".join([token.capitalize() for token in key.split('_')]) - - -def ordered_obj(obj): - """ - Order object for comparison purposes - - :param obj: - :return: - """ - - if isinstance(obj, dict): - return sorted((k, ordered_obj(v)) for k, v in obj.items()) - if isinstance(obj, list): - return sorted(ordered_obj(x) for x in obj) - else: - return obj - - -def set_api_sub_params(params): - """ - Sets module sub-parameters to those expected by the boto3 API. - - :param params: - :return: - """ - - api_params = dict() - - for param in params.keys(): - param_value = params.get(param, None) - if param_value: - api_params[pc(param)] = param_value - - return api_params - - -def validate_params(module, aws): - """ - Performs basic parameter validation. - - :param module: - :param aws: - :return: - """ - - function_name = module.params['lambda_function_arn'] - - # validate function name - if not re.search(r'^[\w\-:]+$', function_name): - module.fail_json( - msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) - ) - if len(function_name) > 64 and not function_name.startswith('arn:aws:lambda:'): - module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - - elif len(function_name) > 140 and function_name.startswith('arn:aws:lambda:'): - module.fail_json(msg='ARN "{0}" exceeds 140 character limit'.format(function_name)) - - # check if 'function_name' needs to be expanded in full ARN format - if not module.params['lambda_function_arn'].startswith('arn:aws:lambda:'): - function_name = module.params['lambda_function_arn'] - module.params['lambda_function_arn'] = 'arn:aws:lambda:{0}:{1}:function:{2}'.format(aws.region, aws.account_id, function_name) - - qualifier = get_qualifier(module) - if qualifier: - function_arn = module.params['lambda_function_arn'] - module.params['lambda_function_arn'] = '{0}:{1}'.format(function_arn, qualifier) - - return - - -def get_qualifier(module): - """ - Returns the function qualifier as a version or alias or None. - - :param module: - :return: - """ - - qualifier = None - if module.params['version'] > 0: - qualifier = str(module.params['version']) - elif module.params['alias']: - qualifier = str(module.params['alias']) - - return qualifier - - -# --------------------------------------------------------------------------------------------------- -# -# Lambda Event Handlers -# -# This section defines a lambda_event_X function where X is an AWS service capable of initiating -# the execution of a Lambda function (pull only). -# -# --------------------------------------------------------------------------------------------------- - -def lambda_event_stream(module, aws): - """ - Adds, updates or deletes lambda stream (DynamoDb, Kinesis) event notifications. - :param module: - :param aws: - :return: - """ - - client = aws.client('lambda') - facts = dict() - changed = False - current_state = 'absent' - state = module.params['state'] - - api_params = dict(FunctionName=module.params['lambda_function_arn']) - - # check if required sub-parameters are present and valid - source_params = module.params['source_params'] - - source_arn = source_params.get('source_arn') - if source_arn: - api_params.update(EventSourceArn=source_arn) - else: - module.fail_json(msg="Source parameter 'source_arn' is required for stream event notification.") - - # check if optional sub-parameters are valid, if present - batch_size = source_params.get('batch_size') - if batch_size: - try: - source_params['batch_size'] = int(batch_size) - except ValueError: - module.fail_json(msg="Source parameter 'batch_size' must be an integer, found: {0}".format(source_params['batch_size'])) - - # optional boolean value needs special treatment as not present does not imply False - source_param_enabled = module.boolean(source_params.get('enabled', 'True')) - - # check if event mapping exist - try: - facts = client.list_event_source_mappings(**api_params)['EventSourceMappings'] - if facts: - current_state = 'present' - except ClientError as e: - module.fail_json(msg='Error retrieving stream event notification configuration: {0}'.format(e)) - - if state == 'present': - if current_state == 'absent': - - starting_position = source_params.get('starting_position') - if starting_position: - api_params.update(StartingPosition=starting_position) - elif module.params.get('event_source') == 'sqs': - # starting position is not required for SQS - pass - else: - module.fail_json(msg="Source parameter 'starting_position' is required for stream event notification.") - - if source_arn: - api_params.update(Enabled=source_param_enabled) - if source_params.get('batch_size'): - api_params.update(BatchSize=source_params.get('batch_size')) - - try: - if not module.check_mode: - facts = client.create_event_source_mapping(**api_params) - changed = True - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error creating stream source event mapping: {0}'.format(e)) - - else: - # current_state is 'present' - api_params = dict(FunctionName=module.params['lambda_function_arn']) - current_mapping = facts[0] - api_params.update(UUID=current_mapping['UUID']) - mapping_changed = False - - # check if anything changed - if source_params.get('batch_size') and source_params['batch_size'] != current_mapping['BatchSize']: - api_params.update(BatchSize=source_params['batch_size']) - mapping_changed = True - - if source_param_enabled is not None: - if source_param_enabled: - if current_mapping['State'] not in ('Enabled', 'Enabling'): - api_params.update(Enabled=True) - mapping_changed = True - else: - if current_mapping['State'] not in ('Disabled', 'Disabling'): - api_params.update(Enabled=False) - mapping_changed = True - - if mapping_changed: - try: - if not module.check_mode: - facts = client.update_event_source_mapping(**api_params) - changed = True - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error updating stream source event mapping: {0}'.format(e)) - - else: - if current_state == 'present': - # remove the stream event mapping - api_params = dict(UUID=facts[0]['UUID']) - - try: - if not module.check_mode: - facts = client.delete_event_source_mapping(**api_params) - changed = True - except (ClientError, ParamValidationError, MissingParametersError) as e: - module.fail_json(msg='Error removing stream source event mapping: {0}'.format(e)) - - return camel_dict_to_snake_dict(dict(changed=changed, events=facts)) - - -def main(): - """Produce a list of function suffixes which handle lambda events.""" - source_choices = ["stream", "sqs"] - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - state=dict(required=False, default='present', choices=['present', 'absent']), - lambda_function_arn=dict(required=True, aliases=['function_name', 'function_arn']), - event_source=dict(required=False, default="stream", choices=source_choices), - source_params=dict(type='dict', required=True), - alias=dict(required=False, default=None), - version=dict(type='int', required=False, default=0), - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['alias', 'version']], - required_together=[] - ) - - # validate dependencies - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for this module.') - - aws = AWSConnection(module, ['lambda']) - - validate_params(module, aws) - - if module.params['event_source'].lower() in ('stream', 'sqs'): - results = lambda_event_stream(module, aws) - else: - module.fail_json(msg='Please select `stream` or `sqs` as the event type') - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lambda_info.py b/lib/ansible/modules/cloud/amazon/lambda_info.py deleted file mode 100644 index d7203c2f95..0000000000 --- a/lib/ansible/modules/cloud/amazon/lambda_info.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: lambda_info -short_description: Gathers AWS Lambda function details -description: - - Gathers various details related to Lambda functions, including aliases, versions and event source mappings. - - Use module M(lambda) to manage the lambda function itself, M(lambda_alias) to manage function aliases and - M(lambda_event) to manage lambda event source mappings. - -version_added: "2.9" - -options: - query: - description: - - Specifies the resource type for which to gather information. Leave blank to retrieve all information. - choices: [ "aliases", "all", "config", "mappings", "policy", "versions" ] - default: "all" - type: str - function_name: - description: - - The name of the lambda function for which information is requested. - aliases: [ "function", "name"] - type: str - event_source_arn: - description: - - When I(query=mappings), this is the Amazon Resource Name (ARN) of the Amazon Kinesis or DynamoDB stream. - type: str -author: Pierre Jodouin (@pjodouin) -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -# Simple example of listing all info for a function -- name: List all for a specific function - lambda_info: - query: all - function_name: myFunction - register: my_function_details -# List all versions of a function -- name: List function versions - lambda_info: - query: versions - function_name: myFunction - register: my_function_versions -# List all lambda function versions -- name: List all function - lambda_info: - query: all - max_items: 20 - register: output -- name: show Lambda information - debug: - msg: "{{ output['function'] }}" -''' - -RETURN = ''' ---- -function: - description: lambda function list - returned: success - type: dict -function.TheName: - description: lambda function information, including event, mapping, and version information - returned: success - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -import json -import datetime -import re - - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def fix_return(node): - """ - fixup returned dictionary - - :param node: - :return: - """ - - if isinstance(node, datetime.datetime): - node_value = str(node) - - elif isinstance(node, list): - node_value = [fix_return(item) for item in node] - - elif isinstance(node, dict): - node_value = dict([(item, fix_return(node[item])) for item in node.keys()]) - - else: - node_value = node - - return node_value - - -def alias_details(client, module): - """ - Returns list of aliases for a specified function. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_info = dict() - - function_name = module.params.get('function_name') - if function_name: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - try: - lambda_info.update(aliases=client.list_aliases(FunctionName=function_name, **params)['Aliases']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(aliases=[]) - else: - module.fail_json_aws(e, msg="Trying to get aliases") - else: - module.fail_json(msg='Parameter function_name required for query=aliases.') - - return {function_name: camel_dict_to_snake_dict(lambda_info)} - - -def all_details(client, module): - """ - Returns all lambda related facts. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - if module.params.get('max_items') or module.params.get('next_marker'): - module.fail_json(msg='Cannot specify max_items nor next_marker for query=all.') - - lambda_info = dict() - - function_name = module.params.get('function_name') - if function_name: - lambda_info[function_name] = {} - lambda_info[function_name].update(config_details(client, module)[function_name]) - lambda_info[function_name].update(alias_details(client, module)[function_name]) - lambda_info[function_name].update(policy_details(client, module)[function_name]) - lambda_info[function_name].update(version_details(client, module)[function_name]) - lambda_info[function_name].update(mapping_details(client, module)[function_name]) - else: - lambda_info.update(config_details(client, module)) - - return lambda_info - - -def config_details(client, module): - """ - Returns configuration details for one or all lambda functions. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_info = dict() - - function_name = module.params.get('function_name') - if function_name: - try: - lambda_info.update(client.get_function_configuration(FunctionName=function_name)) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(function={}) - else: - module.fail_json_aws(e, msg="Trying to get {0} configuration".format(function_name)) - else: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_info.update(function_list=client.list_functions(**params)['Functions']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(function_list=[]) - else: - module.fail_json_aws(e, msg="Trying to get function list") - - functions = dict() - for func in lambda_info.pop('function_list', []): - functions[func['FunctionName']] = camel_dict_to_snake_dict(func) - return functions - - return {function_name: camel_dict_to_snake_dict(lambda_info)} - - -def mapping_details(client, module): - """ - Returns all lambda event source mappings. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_info = dict() - params = dict() - function_name = module.params.get('function_name') - - if function_name: - params['FunctionName'] = module.params.get('function_name') - - if module.params.get('event_source_arn'): - params['EventSourceArn'] = module.params.get('event_source_arn') - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_info.update(mappings=client.list_event_source_mappings(**params)['EventSourceMappings']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(mappings=[]) - else: - module.fail_json_aws(e, msg="Trying to get source event mappings") - - if function_name: - return {function_name: camel_dict_to_snake_dict(lambda_info)} - - return camel_dict_to_snake_dict(lambda_info) - - -def policy_details(client, module): - """ - Returns policy attached to a lambda function. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - if module.params.get('max_items') or module.params.get('next_marker'): - module.fail_json(msg='Cannot specify max_items nor next_marker for query=policy.') - - lambda_info = dict() - - function_name = module.params.get('function_name') - if function_name: - try: - # get_policy returns a JSON string so must convert to dict before reassigning to its key - lambda_info.update(policy=json.loads(client.get_policy(FunctionName=function_name)['Policy'])) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(policy={}) - else: - module.fail_json_aws(e, msg="Trying to get {0} policy".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=policy.') - - return {function_name: camel_dict_to_snake_dict(lambda_info)} - - -def version_details(client, module): - """ - Returns all lambda function versions. - - :param client: AWS API client reference (boto3) - :param module: Ansible module reference - :return dict: - """ - - lambda_info = dict() - - function_name = module.params.get('function_name') - if function_name: - params = dict() - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - try: - lambda_info.update(versions=client.list_versions_by_function(FunctionName=function_name, **params)['Versions']) - except ClientError as e: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - lambda_info.update(versions=[]) - else: - module.fail_json_aws(e, msg="Trying to get {0} versions".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=versions.') - - return {function_name: camel_dict_to_snake_dict(lambda_info)} - - -def main(): - """ - Main entry point. - - :return dict: ansible facts - """ - argument_spec = dict( - function_name=dict(required=False, default=None, aliases=['function', 'name']), - query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions'], default='all'), - event_source_arn=dict(required=False, default=None) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[], - required_together=[] - ) - - # validate function_name if present - function_name = module.params['function_name'] - if function_name: - if not re.search(r"^[\w\-:]+$", function_name): - module.fail_json( - msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) - ) - if len(function_name) > 64: - module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - - client = module.client('lambda') - - invocations = dict( - aliases='alias_details', - all='all_details', - config='config_details', - mappings='mapping_details', - policy='policy_details', - versions='version_details', - ) - - this_module_function = globals()[invocations[module.params['query']]] - all_facts = fix_return(this_module_function(client, module)) - - results = dict(function=all_facts, changed=False) - - if module.check_mode: - results['msg'] = 'Check mode set but ignored for fact gathering only.' - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lambda_policy.py b/lib/ansible/modules/cloud/amazon/lambda_policy.py deleted file mode 100644 index 2c7342411b..0000000000 --- a/lib/ansible/modules/cloud/amazon/lambda_policy.py +++ /dev/null @@ -1,439 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2016, Pierre Jodouin <pjodouin@virtualcomputing.solutions> -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: lambda_policy -short_description: Creates, updates or deletes AWS Lambda policy statements. -description: - - This module allows the management of AWS Lambda policy statements. - - It is idempotent and supports "Check" mode. - - Use module M(lambda) to manage the lambda function itself, M(lambda_alias) to manage function aliases, - M(lambda_event) to manage event source mappings such as Kinesis streams, M(execute_lambda) to execute a - lambda function and M(lambda_info) to gather information relating to one or more lambda functions. - -version_added: "2.4" - -author: - - Pierre Jodouin (@pjodouin) - - Michael De La Rue (@mikedlr) -options: - function_name: - description: - - "Name of the Lambda function whose resource policy you are updating by adding a new permission." - - "You can specify a function name (for example, Thumbnail ) or you can specify Amazon Resource Name (ARN) of the" - - "function (for example, C(arn:aws:lambda:us-west-2:account-id:function:ThumbNail) ). AWS Lambda also allows you to" - - "specify partial ARN (for example, C(account-id:Thumbnail) ). Note that the length constraint applies only to the" - - "ARN. If you specify only the function name, it is limited to 64 character in length." - required: true - aliases: ['lambda_function_arn', 'function_arn'] - type: str - - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - - alias: - description: - - Name of the function alias. Mutually exclusive with I(version). - type: str - - version: - description: - - Version of the Lambda function. Mutually exclusive with I(alias). - type: int - - statement_id: - description: - - A unique statement identifier. - required: true - aliases: ['sid'] - type: str - - action: - description: - - "The AWS Lambda action you want to allow in this statement. Each Lambda action is a string starting with - lambda: followed by the API name (see Operations ). For example, C(lambda:CreateFunction) . You can use wildcard - (C(lambda:*)) to grant permission for all AWS Lambda actions." - required: true - type: str - - principal: - description: - - "The principal who is getting this permission. It can be Amazon S3 service Principal (s3.amazonaws.com ) if - you want Amazon S3 to invoke the function, an AWS account ID if you are granting cross-account permission, or - any valid AWS service principal such as sns.amazonaws.com . For example, you might want to allow a custom - application in another AWS account to push events to AWS Lambda by invoking your function." - required: true - type: str - - source_arn: - description: - - This is optional; however, when granting Amazon S3 permission to invoke your function, you should specify this - field with the bucket Amazon Resource Name (ARN) as its value. This ensures that only events generated from - the specified bucket can invoke the function. - type: str - - source_account: - description: - - The AWS account ID (without a hyphen) of the source owner. For example, if I(source_arn) identifies a bucket, - then this is the bucket owner's account ID. You can use this additional condition to ensure the bucket you - specify is owned by a specific account (it is possible the bucket owner deleted the bucket and some other AWS - account created the bucket). You can also use this condition to specify all sources (that is, you don't - specify the I(source_arn) ) owned by a specific account. - type: str - - event_source_token: - description: - - Token string representing source ARN or account. Mutually exclusive with I(source_arn) or I(source_account). - type: str - -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -- hosts: localhost - gather_facts: no - vars: - state: present - tasks: - - name: Lambda S3 event notification - lambda_policy: - state: "{{ state | default('present') }}" - function_name: functionName - alias: Dev - statement_id: lambda-s3-myBucket-create-data-log - action: lambda:InvokeFunction - principal: s3.amazonaws.com - source_arn: arn:aws:s3:eu-central-1:123456789012:bucketName - source_account: 123456789012 - register: lambda_policy_action - - - name: show results - debug: - var: lambda_policy_action - -''' - -RETURN = ''' ---- -lambda_policy_action: - description: describes what action was taken - returned: success - type: str -''' - -import json -import re -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - from botocore.exceptions import ClientError -except Exception: - pass # caught by AnsibleAWSModule - - -def pc(key): - """ - Changes python key into Pascal case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'. - - :param key: - :return: - """ - - return "".join([token.capitalize() for token in key.split('_')]) - - -def policy_equal(module, current_statement): - for param in ('action', 'principal', 'source_arn', 'source_account', 'event_source_token'): - if module.params.get(param) != current_statement.get(param): - return False - - return True - - -def set_api_params(module, module_params): - """ - Sets module parameters to those expected by the boto3 API. - - :param module: - :param module_params: - :return: - """ - - api_params = dict() - - for param in module_params: - module_param = module.params.get(param) - if module_param is not None: - api_params[pc(param)] = module_param - - return api_params - - -def validate_params(module): - """ - Performs parameter validation beyond the module framework's validation. - - :param module: - :return: - """ - - function_name = module.params['function_name'] - - # validate function name - if function_name.startswith('arn:'): - if not re.search(r'^[\w\-:]+$', function_name): - module.fail_json( - msg='ARN {0} is invalid. ARNs must contain only alphanumeric characters, hyphens and colons.'.format(function_name) - ) - if len(function_name) > 140: - module.fail_json(msg='ARN name "{0}" exceeds 140 character limit'.format(function_name)) - else: - if not re.search(r'^[\w\-]+$', function_name): - module.fail_json( - msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format( - function_name) - ) - if len(function_name) > 64: - module.fail_json( - msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - - -def get_qualifier(module): - """ - Returns the function qualifier as a version or alias or None. - - :param module: - :return: - """ - - if module.params.get('version') is not None: - return to_native(module.params['version']) - elif module.params['alias']: - return to_native(module.params['alias']) - - return None - - -def extract_statement(policy, sid): - """return flattened single policy statement from a policy - - If a policy statement is present in the policy extract it and - return it in a flattened form. Otherwise return an empty - dictionary. - """ - if 'Statement' not in policy: - return {} - policy_statement = {} - # Now that we have the policy, check if required permission statement is present and flatten to - # simple dictionary if found. - for statement in policy['Statement']: - if statement['Sid'] == sid: - policy_statement['action'] = statement['Action'] - try: - policy_statement['principal'] = statement['Principal']['Service'] - except KeyError: - pass - try: - policy_statement['principal'] = statement['Principal']['AWS'] - except KeyError: - pass - try: - policy_statement['source_arn'] = statement['Condition']['ArnLike']['AWS:SourceArn'] - except KeyError: - pass - try: - policy_statement['source_account'] = statement['Condition']['StringEquals']['AWS:SourceAccount'] - except KeyError: - pass - try: - policy_statement['event_source_token'] = statement['Condition']['StringEquals']['lambda:EventSourceToken'] - except KeyError: - pass - break - - return policy_statement - - -def get_policy_statement(module, client): - """Checks that policy exists and if so, that statement ID is present or absent. - - :param module: - :param client: - :return: - """ - sid = module.params['statement_id'] - - # set API parameters - api_params = set_api_params(module, ('function_name', )) - qualifier = get_qualifier(module) - if qualifier: - api_params.update(Qualifier=qualifier) - - policy_results = None - # check if function policy exists - try: - policy_results = client.get_policy(**api_params) - except ClientError as e: - try: - if e.response['Error']['Code'] == 'ResourceNotFoundException': - return {} - except AttributeError: # catches ClientErrors without response, e.g. fail before connect - pass - module.fail_json_aws(e, msg="retrieving function policy") - except Exception as e: - module.fail_json_aws(e, msg="retrieving function policy") - - # get_policy returns a JSON string so must convert to dict before reassigning to its key - policy = json.loads(policy_results.get('Policy', '{}')) - return extract_statement(policy, sid) - - -def add_policy_permission(module, client): - """ - Adds a permission statement to the policy. - - :param module: - :param aws: - :return: - """ - - changed = False - - # set API parameters - params = ( - 'function_name', - 'statement_id', - 'action', - 'principal', - 'source_arn', - 'source_account', - 'event_source_token') - api_params = set_api_params(module, params) - qualifier = get_qualifier(module) - if qualifier: - api_params.update(Qualifier=qualifier) - - if not module.check_mode: - try: - client.add_permission(**api_params) - except Exception as e: - module.fail_json_aws(e, msg="adding permission to policy") - changed = True - - return changed - - -def remove_policy_permission(module, client): - """ - Removed a permission statement from the policy. - - :param module: - :param aws: - :return: - """ - - changed = False - - # set API parameters - api_params = set_api_params(module, ('function_name', 'statement_id')) - qualifier = get_qualifier(module) - if qualifier: - api_params.update(Qualifier=qualifier) - - try: - if not module.check_mode: - client.remove_permission(**api_params) - changed = True - except Exception as e: - module.fail_json_aws(e, msg="removing permission from policy") - - return changed - - -def manage_state(module, lambda_client): - changed = False - current_state = 'absent' - state = module.params['state'] - action_taken = 'none' - - # check if the policy exists - current_policy_statement = get_policy_statement(module, lambda_client) - if current_policy_statement: - current_state = 'present' - - if state == 'present': - if current_state == 'present' and not policy_equal(module, current_policy_statement): - remove_policy_permission(module, lambda_client) - changed = add_policy_permission(module, lambda_client) - action_taken = 'updated' - if not current_state == 'present': - changed = add_policy_permission(module, lambda_client) - action_taken = 'added' - elif current_state == 'present': - # remove the policy statement - changed = remove_policy_permission(module, lambda_client) - action_taken = 'deleted' - - return dict(changed=changed, ansible_facts=dict(lambda_policy_action=action_taken)) - - -def setup_module_object(): - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent']), - function_name=dict(required=True, aliases=['lambda_function_arn', 'function_arn']), - statement_id=dict(required=True, aliases=['sid']), - alias=dict(), - version=dict(type='int'), - action=dict(required=True, ), - principal=dict(required=True, ), - source_arn=dict(), - source_account=dict(), - event_source_token=dict(), - ) - - return AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['alias', 'version'], - ['event_source_token', 'source_arn'], - ['event_source_token', 'source_account']], - ) - - -def main(): - """ - Main entry point. - - :return dict: ansible facts - """ - - module = setup_module_object() - client = module.client('lambda') - validate_params(module) - results = manage_state(module, client) - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/lightsail.py b/lib/ansible/modules/cloud/amazon/lightsail.py deleted file mode 100644 index 162fca7f4b..0000000000 --- a/lib/ansible/modules/cloud/amazon/lightsail.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: lightsail -short_description: Manage instances in AWS Lightsail -description: - - Manage instances in AWS Lightsail. - - Instance tagging is not yet supported in this module. -version_added: "2.4" -author: - - "Nick Ball (@nickball)" - - "Prasad Katti (@prasadkatti)" -options: - state: - description: - - Indicate desired state of the target. - - I(rebooted) and I(restarted) are aliases. - default: present - choices: ['present', 'absent', 'running', 'restarted', 'rebooted', 'stopped'] - type: str - name: - description: Name of the instance. - required: true - type: str - zone: - description: - - AWS availability zone in which to launch the instance. - - Required when I(state=present) - type: str - blueprint_id: - description: - - ID of the instance blueprint image. - - Required when I(state=present) - type: str - bundle_id: - description: - - Bundle of specification info for the instance. - - Required when I(state=present). - type: str - user_data: - description: - - Launch script that can configure the instance with additional data. - type: str - key_pair_name: - description: - - Name of the key pair to use with the instance. - - If I(state=present) and a key_pair_name is not provided, the default keypair from the region will be used. - type: str - wait: - description: - - Wait for the instance to be in state 'running' before returning. - - If I(wait=false) an ip_address may not be returned. - - Has no effect when I(state=rebooted) or I(state=absent). - type: bool - default: true - wait_timeout: - description: - - How long before I(wait) gives up, in seconds. - default: 300 - type: int - -requirements: - - boto3 - -extends_documentation_fragment: - - aws - - ec2 -''' - - -EXAMPLES = ''' -# Create a new Lightsail instance -- lightsail: - state: present - name: my_instance - region: us-east-1 - zone: us-east-1a - blueprint_id: ubuntu_16_04 - bundle_id: nano_1_0 - key_pair_name: id_rsa - user_data: " echo 'hello world' > /home/ubuntu/test.txt" - register: my_instance - -# Delete an instance -- lightsail: - state: absent - region: us-east-1 - name: my_instance - -''' - -RETURN = ''' -changed: - description: if a snapshot has been modified/created - returned: always - type: bool - sample: - changed: true -instance: - description: instance data - returned: always - type: dict - sample: - arn: "arn:aws:lightsail:us-east-1:448830907657:Instance/1fef0175-d6c8-480e-84fa-214f969cda87" - blueprint_id: "ubuntu_16_04" - blueprint_name: "Ubuntu" - bundle_id: "nano_1_0" - created_at: "2017-03-27T08:38:59.714000-04:00" - hardware: - cpu_count: 1 - ram_size_in_gb: 0.5 - is_static_ip: false - location: - availability_zone: "us-east-1a" - region_name: "us-east-1" - name: "my_instance" - networking: - monthly_transfer: - gb_per_month_allocated: 1024 - ports: - - access_direction: "inbound" - access_from: "Anywhere (0.0.0.0/0)" - access_type: "public" - common_name: "" - from_port: 80 - protocol: tcp - to_port: 80 - - access_direction: "inbound" - access_from: "Anywhere (0.0.0.0/0)" - access_type: "public" - common_name: "" - from_port: 22 - protocol: tcp - to_port: 22 - private_ip_address: "172.26.8.14" - public_ip_address: "34.207.152.202" - resource_type: "Instance" - ssh_key_name: "keypair" - state: - code: 16 - name: running - support_code: "588307843083/i-0997c97831ee21e33" - username: "ubuntu" -''' - -import time - -try: - import botocore -except ImportError: - # will be caught by AnsibleAWSModule - pass - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -def find_instance_info(module, client, instance_name, fail_if_not_found=False): - - try: - res = client.get_instance(instanceName=instance_name) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'NotFoundException' and not fail_if_not_found: - return None - module.fail_json_aws(e) - return res['instance'] - - -def wait_for_instance_state(module, client, instance_name, states): - """ - `states` is a list of instance states that we are waiting for. - """ - - wait_timeout = module.params.get('wait_timeout') - wait_max = time.time() + wait_timeout - while wait_max > time.time(): - try: - instance = find_instance_info(module, client, instance_name) - if instance['state']['name'] in states: - break - time.sleep(5) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - else: - module.fail_json(msg='Timed out waiting for instance "{0}" to get to one of the following states -' - ' {1}'.format(instance_name, states)) - - -def create_instance(module, client, instance_name): - - inst = find_instance_info(module, client, instance_name) - if inst: - module.exit_json(changed=False, instance=camel_dict_to_snake_dict(inst)) - else: - create_params = {'instanceNames': [instance_name], - 'availabilityZone': module.params.get('zone'), - 'blueprintId': module.params.get('blueprint_id'), - 'bundleId': module.params.get('bundle_id'), - 'userData': module.params.get('user_data')} - - key_pair_name = module.params.get('key_pair_name') - if key_pair_name: - create_params['keyPairName'] = key_pair_name - - try: - client.create_instances(**create_params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - wait = module.params.get('wait') - if wait: - desired_states = ['running'] - wait_for_instance_state(module, client, instance_name, desired_states) - inst = find_instance_info(module, client, instance_name, fail_if_not_found=True) - - module.exit_json(changed=True, instance=camel_dict_to_snake_dict(inst)) - - -def delete_instance(module, client, instance_name): - - changed = False - - inst = find_instance_info(module, client, instance_name) - if inst is None: - module.exit_json(changed=changed, instance={}) - - # Wait for instance to exit transition state before deleting - desired_states = ['running', 'stopped'] - wait_for_instance_state(module, client, instance_name, desired_states) - - try: - client.delete_instance(instanceName=instance_name) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(inst)) - - -def restart_instance(module, client, instance_name): - """ - Reboot an existing instance - Wait will not apply here as this is an OS-level operation - """ - - changed = False - - inst = find_instance_info(module, client, instance_name, fail_if_not_found=True) - - try: - client.reboot_instance(instanceName=instance_name) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(inst)) - - -def start_or_stop_instance(module, client, instance_name, state): - """ - Start or stop an existing instance - """ - - changed = False - - inst = find_instance_info(module, client, instance_name, fail_if_not_found=True) - - # Wait for instance to exit transition state before state change - desired_states = ['running', 'stopped'] - wait_for_instance_state(module, client, instance_name, desired_states) - - # Try state change - if inst and inst['state']['name'] != state: - try: - if state == 'running': - client.start_instance(instanceName=instance_name) - else: - client.stop_instance(instanceName=instance_name) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - changed = True - # Grab current instance info - inst = find_instance_info(module, client, instance_name) - - wait = module.params.get('wait') - if wait: - desired_states = [state] - wait_for_instance_state(module, client, instance_name, desired_states) - inst = find_instance_info(module, client, instance_name, fail_if_not_found=True) - - module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(inst)) - - -def main(): - - argument_spec = dict( - name=dict(type='str', required=True), - state=dict(type='str', default='present', choices=['present', 'absent', 'stopped', 'running', 'restarted', - 'rebooted']), - zone=dict(type='str'), - blueprint_id=dict(type='str'), - bundle_id=dict(type='str'), - key_pair_name=dict(type='str'), - user_data=dict(type='str', default=''), - wait=dict(type='bool', default=True), - wait_timeout=dict(default=300, type='int'), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['state', 'present', ('zone', 'blueprint_id', 'bundle_id')]]) - - client = module.client('lightsail') - - name = module.params.get('name') - state = module.params.get('state') - - if state == 'present': - create_instance(module, client, name) - elif state == 'absent': - delete_instance(module, client, name) - elif state in ('running', 'stopped'): - start_or_stop_instance(module, client, name, state) - elif state in ('restarted', 'rebooted'): - restart_instance(module, client, name) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds.py b/lib/ansible/modules/cloud/amazon/rds.py deleted file mode 100644 index 04660cb8ff..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds.py +++ /dev/null @@ -1,1405 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: rds -version_added: "1.3" -short_description: create, delete, or modify Amazon rds instances, rds snapshots, and related facts -description: - - Creates, deletes, or modifies rds resources. - - When creating an instance it can be either a new instance or a read-only replica of an existing instance. - - This module has a dependency on python-boto >= 2.5 and will soon be deprecated. - - The 'promote' command requires boto >= 2.18.0. Certain features such as tags rely on boto.rds2 (boto >= 2.26.0). - - Please use boto3 based M(rds_instance) instead. -options: - command: - description: - - Specifies the action to take. The 'reboot' option is available starting at version 2.0. - required: true - choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'reboot', 'restore' ] - type: str - instance_name: - description: - - Database instance identifier. - - Required except when using I(command=facts) or I(command=delete) on just a snapshot. - type: str - source_instance: - description: - - Name of the database to replicate. - - Used only when I(command=replicate). - type: str - db_engine: - description: - - The type of database. - - Used only when I(command=create). - - mariadb was added in version 2.2. - choices: ['mariadb', 'MySQL', 'oracle-se1', 'oracle-se2', 'oracle-se', 'oracle-ee', - 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres', 'aurora'] - type: str - size: - description: - - Size in gigabytes of the initial storage for the DB instance. - - Used only when I(command=create) or I(command=modify). - type: str - instance_type: - description: - - The instance type of the database. - - If not specified then the replica inherits the same instance type as the source instance. - - Required when I(command=create). - - Optional when I(command=replicate), I(command=modify) or I(command=restore). - aliases: ['type'] - type: str - username: - description: - - Master database username. - - Used only when I(command=create). - type: str - password: - description: - - Password for the master database username. - - Used only when I(command=create) or I(command=modify). - type: str - db_name: - description: - - Name of a database to create within the instance. - - If not specified then no database is created. - - Used only when I(command=create). - type: str - engine_version: - description: - - Version number of the database engine to use. - - If not specified then the current Amazon RDS default engine version is used - - Used only when I(command=create). - type: str - parameter_group: - description: - - Name of the DB parameter group to associate with this instance. - - If omitted then the RDS default DBParameterGroup will be used. - - Used only when I(command=create) or I(command=modify). - type: str - license_model: - description: - - The license model for this DB instance. - - Used only when I(command=create) or I(command=restore). - choices: [ 'license-included', 'bring-your-own-license', 'general-public-license', 'postgresql-license' ] - type: str - multi_zone: - description: - - Specifies if this is a Multi-availability-zone deployment. - - Can not be used in conjunction with I(zone) parameter. - - Used only when I(command=create) or I(command=modify). - type: bool - iops: - description: - - Specifies the number of IOPS for the instance. - - Used only when I(command=create) or I(command=modify). - - Must be an integer greater than 1000. - type: str - security_groups: - description: - - Comma separated list of one or more security groups. - - Used only when I(command=create) or I(command=modify). - type: str - vpc_security_groups: - description: - - Comma separated list of one or more vpc security group ids. - - Also requires I(subnet) to be specified. - - Used only when I(command=create) or I(command=modify). - type: list - elements: str - port: - description: - - Port number that the DB instance uses for connections. - - Used only when I(command=create) or I(command=replicate). - - 'Defaults to the standard ports for each I(db_engine): C(3306) for MySQL and MariaDB, C(1521) for Oracle - C(1433) for SQL Server, C(5432) for PostgreSQL.' - type: int - upgrade: - description: - - Indicates that minor version upgrades should be applied automatically. - - Used only when I(command=create) or I(command=modify) or I(command=restore) or I(command=replicate). - type: bool - default: false - option_group: - description: - - The name of the option group to use. - - If not specified then the default option group is used. - - Used only when I(command=create). - type: str - maint_window: - description: - - 'Maintenance window in format of C(ddd:hh24:mi-ddd:hh24:mi). (Example: C(Mon:22:00-Mon:23:15))' - - Times are specified in UTC. - - If not specified then a random maintenance window is assigned. - - Used only when I(command=create) or I(command=modify). - type: str - backup_window: - description: - - 'Backup window in format of C(hh24:mi-hh24:mi). (Example: C(18:00-20:30))' - - Times are specified in UTC. - - If not specified then a random backup window is assigned. - - Used only when command=create or command=modify. - type: str - backup_retention: - description: - - Number of days backups are retained. - - Set to 0 to disable backups. - - Default is 1 day. - - 'Valid range: 0-35.' - - Used only when I(command=create) or I(command=modify). - type: str - zone: - description: - - availability zone in which to launch the instance. - - Used only when I(command=create), I(command=replicate) or I(command=restore). - - Can not be used in conjunction with I(multi_zone) parameter. - aliases: ['aws_zone', 'ec2_zone'] - type: str - subnet: - description: - - VPC subnet group. - - If specified then a VPC instance is created. - - Used only when I(command=create). - type: str - snapshot: - description: - - Name of snapshot to take. - - When I(command=delete), if no I(snapshot) name is provided then no snapshot is taken. - - When I(command=delete), if no I(instance_name) is provided the snapshot is deleted. - - Used with I(command=facts), I(command=delete) or I(command=snapshot). - type: str - wait: - description: - - When I(command=create), replicate, modify or restore then wait for the database to enter the 'available' state. - - When I(command=delete), wait for the database to be terminated. - type: bool - default: false - wait_timeout: - description: - - How long before wait gives up, in seconds. - - Used when I(wait=true). - default: 300 - type: int - apply_immediately: - description: - - When I(apply_immediately=trye), the modifications will be applied as soon as possible rather than waiting for the - next preferred maintenance window. - - Used only when I(command=modify). - type: bool - default: false - force_failover: - description: - - If enabled, the reboot is done using a MultiAZ failover. - - Used only when I(command=reboot). - type: bool - default: false - version_added: "2.0" - new_instance_name: - description: - - Name to rename an instance to. - - Used only when I(command=modify). - type: str - version_added: "1.5" - character_set_name: - description: - - Associate the DB instance with a specified character set. - - Used with I(command=create). - version_added: "1.9" - type: str - publicly_accessible: - description: - - Explicitly set whether the resource should be publicly accessible or not. - - Used with I(command=create), I(command=replicate). - - Requires boto >= 2.26.0 - type: str - version_added: "1.9" - tags: - description: - - tags dict to apply to a resource. - - Used with I(command=create), I(command=replicate), I(command=restore). - - Requires boto >= 2.26.0 - type: dict - version_added: "1.9" -requirements: - - "python >= 2.6" - - "boto" -author: - - "Bruce Pennypacker (@bpennypacker)" - - "Will Thames (@willthames)" -extends_documentation_fragment: - - aws - - ec2 -''' - -# FIXME: the command stuff needs a 'state' like alias to make things consistent -- MPD - -EXAMPLES = ''' -# Basic mysql provisioning example -- rds: - command: create - instance_name: new-database - db_engine: MySQL - size: 10 - instance_type: db.m1.small - username: mysql_admin - password: 1nsecure - tags: - Environment: testing - Application: cms - -# Create a read-only replica and wait for it to become available -- rds: - command: replicate - instance_name: new-database-replica - source_instance: new_database - wait: yes - wait_timeout: 600 - -# Delete an instance, but create a snapshot before doing so -- rds: - command: delete - instance_name: new-database - snapshot: new_database_snapshot - -# Get facts about an instance -- rds: - command: facts - instance_name: new-database - register: new_database_facts - -# Rename an instance and wait for the change to take effect -- rds: - command: modify - instance_name: new-database - new_instance_name: renamed-database - wait: yes - -# Reboot an instance and wait for it to become available again -- rds: - command: reboot - instance_name: database - wait: yes - -# Restore a Postgres db instance from a snapshot, wait for it to become available again, and -# then modify it to add your security group. Also, display the new endpoint. -# Note that the "publicly_accessible" option is allowed here just as it is in the AWS CLI -- local_action: - module: rds - command: restore - snapshot: mypostgres-snapshot - instance_name: MyNewInstanceName - region: us-west-2 - zone: us-west-2b - subnet: default-vpc-xx441xxx - publicly_accessible: yes - wait: yes - wait_timeout: 600 - tags: - Name: pg1_test_name_tag - register: rds - -- local_action: - module: rds - command: modify - instance_name: MyNewInstanceName - region: us-west-2 - vpc_security_groups: sg-xxx945xx - -- debug: - msg: "The new db endpoint is {{ rds.instance.endpoint }}" -''' - -RETURN = ''' -instance: - description: the rds instance - returned: always - type: complex - contains: - engine: - description: the name of the database engine - returned: when RDS instance exists - type: str - sample: "oracle-se" - engine_version: - description: the version of the database engine - returned: when RDS instance exists - type: str - sample: "11.2.0.4.v6" - license_model: - description: the license model information - returned: when RDS instance exists - type: str - sample: "bring-your-own-license" - character_set_name: - description: the name of the character set that this instance is associated with - returned: when RDS instance exists - type: str - sample: "AL32UTF8" - allocated_storage: - description: the allocated storage size in gigabytes (GB) - returned: when RDS instance exists - type: str - sample: "100" - publicly_accessible: - description: the accessibility options for the DB instance - returned: when RDS instance exists - type: bool - sample: "true" - latest_restorable_time: - description: the latest time to which a database can be restored with point-in-time restore - returned: when RDS instance exists - type: str - sample: "1489707802.0" - secondary_availability_zone: - description: the name of the secondary AZ for a DB instance with multi-AZ support - returned: when RDS instance exists and is multy-AZ - type: str - sample: "eu-west-1b" - backup_window: - description: the daily time range during which automated backups are created if automated backups are enabled - returned: when RDS instance exists and automated backups are enabled - type: str - sample: "03:00-03:30" - auto_minor_version_upgrade: - description: indicates that minor engine upgrades will be applied automatically to the DB instance during the maintenance window - returned: when RDS instance exists - type: bool - sample: "true" - read_replica_source_dbinstance_identifier: - description: the identifier of the source DB instance if this RDS instance is a read replica - returned: when read replica RDS instance exists - type: str - sample: "null" - db_name: - description: the name of the database to create when the DB instance is created - returned: when RDS instance exists - type: str - sample: "ASERTG" - endpoint: - description: the endpoint uri of the database instance - returned: when RDS instance exists - type: str - sample: "my-ansible-database.asdfaosdgih.us-east-1.rds.amazonaws.com" - port: - description: the listening port of the database instance - returned: when RDS instance exists - type: int - sample: 3306 - parameter_groups: - description: the list of DB parameter groups applied to this RDS instance - returned: when RDS instance exists and parameter groups are defined - type: complex - contains: - parameter_apply_status: - description: the status of parameter updates - returned: when RDS instance exists - type: str - sample: "in-sync" - parameter_group_name: - description: the name of the DP parameter group - returned: when RDS instance exists - type: str - sample: "testawsrpprodb01spfile-1ujg7nrs7sgyz" - option_groups: - description: the list of option group memberships for this RDS instance - returned: when RDS instance exists - type: complex - contains: - option_group_name: - description: the option group name for this RDS instance - returned: when RDS instance exists - type: str - sample: "default:oracle-se-11-2" - status: - description: the status of the RDS instance's option group membership - returned: when RDS instance exists - type: str - sample: "in-sync" - pending_modified_values: - description: a dictionary of changes to the RDS instance that are pending - returned: when RDS instance exists - type: complex - contains: - db_instance_class: - description: the new DB instance class for this RDS instance that will be applied or is in progress - returned: when RDS instance exists - type: str - sample: "null" - db_instance_identifier: - description: the new DB instance identifier this RDS instance that will be applied or is in progress - returned: when RDS instance exists - type: str - sample: "null" - allocated_storage: - description: the new allocated storage size for this RDS instance that will be applied or is in progress - returned: when RDS instance exists - type: str - sample: "null" - backup_retention_period: - description: the pending number of days for which automated backups are retained - returned: when RDS instance exists - type: str - sample: "null" - engine_version: - description: indicates the database engine version - returned: when RDS instance exists - type: str - sample: "null" - iops: - description: the new provisioned IOPS value for this RDS instance that will be applied or is being applied - returned: when RDS instance exists - type: str - sample: "null" - master_user_password: - description: the pending or in-progress change of the master credentials for this RDS instance - returned: when RDS instance exists - type: str - sample: "null" - multi_az: - description: indicates that the single-AZ RDS instance is to change to a multi-AZ deployment - returned: when RDS instance exists - type: str - sample: "null" - port: - description: specifies the pending port for this RDS instance - returned: when RDS instance exists - type: str - sample: "null" - db_subnet_groups: - description: information on the subnet group associated with this RDS instance - returned: when RDS instance exists - type: complex - contains: - description: - description: the subnet group associated with the DB instance - returned: when RDS instance exists - type: str - sample: "Subnets for the UAT RDS SQL DB Instance" - name: - description: the name of the DB subnet group - returned: when RDS instance exists - type: str - sample: "samplesubnetgrouprds-j6paiqkxqp4z" - status: - description: the status of the DB subnet group - returned: when RDS instance exists - type: str - sample: "complete" - subnets: - description: the description of the DB subnet group - returned: when RDS instance exists - type: complex - contains: - availability_zone: - description: subnet availability zone information - returned: when RDS instance exists - type: complex - contains: - name: - description: availability zone - returned: when RDS instance exists - type: str - sample: "eu-west-1b" - provisioned_iops_capable: - description: whether provisioned iops are available in AZ subnet - returned: when RDS instance exists - type: bool - sample: "false" - identifier: - description: the identifier of the subnet - returned: when RDS instance exists - type: str - sample: "subnet-3fdba63e" - status: - description: the status of the subnet - returned: when RDS instance exists - type: str - sample: "active" -''' - -import time - -try: - import boto.rds - import boto.exception -except ImportError: - pass # Taken care of by ec2.HAS_BOTO - -try: - import boto.rds2 - import boto.rds2.exceptions - HAS_RDS2 = True -except ImportError: - HAS_RDS2 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import AWSRetry -from ansible.module_utils.ec2 import HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info - - -DEFAULT_PORTS = { - 'aurora': 3306, - 'mariadb': 3306, - 'mysql': 3306, - 'oracle': 1521, - 'sqlserver': 1433, - 'postgres': 5432, -} - - -class RDSException(Exception): - def __init__(self, exc): - if hasattr(exc, 'error_message') and exc.error_message: - self.message = exc.error_message - self.code = exc.error_code - elif hasattr(exc, 'body') and 'Error' in exc.body: - self.message = exc.body['Error']['Message'] - self.code = exc.body['Error']['Code'] - else: - self.message = str(exc) - self.code = 'Unknown Error' - - -class RDSConnection: - def __init__(self, module, region, **aws_connect_params): - try: - self.connection = connect_to_aws(boto.rds, region, **aws_connect_params) - except boto.exception.BotoServerError as e: - module.fail_json(msg=e.error_message) - - def get_db_instance(self, instancename): - try: - return RDSDBInstance(self.connection.get_all_dbinstances(instancename)[0]) - except boto.exception.BotoServerError: - return None - - def get_db_snapshot(self, snapshotid): - try: - return RDSSnapshot(self.connection.get_all_dbsnapshots(snapshot_id=snapshotid)[0]) - except boto.exception.BotoServerError: - return None - - def create_db_instance(self, instance_name, size, instance_class, db_engine, - username, password, **params): - params['engine'] = db_engine - try: - result = self.connection.create_dbinstance(instance_name, size, instance_class, - username, password, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def create_db_instance_read_replica(self, instance_name, source_instance, **params): - try: - result = self.connection.createdb_instance_read_replica(instance_name, source_instance, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def delete_db_instance(self, instance_name, **params): - try: - result = self.connection.delete_dbinstance(instance_name, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def delete_db_snapshot(self, snapshot): - try: - result = self.connection.delete_dbsnapshot(snapshot) - return RDSSnapshot(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def modify_db_instance(self, instance_name, **params): - try: - result = self.connection.modify_dbinstance(instance_name, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def reboot_db_instance(self, instance_name, **params): - try: - result = self.connection.reboot_dbinstance(instance_name) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params): - try: - result = self.connection.restore_dbinstance_from_dbsnapshot(snapshot, instance_name, instance_type, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def create_db_snapshot(self, snapshot, instance_name, **params): - try: - result = self.connection.create_dbsnapshot(snapshot, instance_name) - return RDSSnapshot(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def promote_read_replica(self, instance_name, **params): - try: - result = self.connection.promote_read_replica(instance_name, **params) - return RDSDBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - -class RDS2Connection: - def __init__(self, module, region, **aws_connect_params): - try: - self.connection = connect_to_aws(boto.rds2, region, **aws_connect_params) - except boto.exception.BotoServerError as e: - module.fail_json(msg=e.error_message) - - def get_db_instance(self, instancename): - try: - dbinstances = self.connection.describe_db_instances( - db_instance_identifier=instancename - )['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'] - result = RDS2DBInstance(dbinstances[0]) - return result - except boto.rds2.exceptions.DBInstanceNotFound as e: - return None - except Exception as e: - raise e - - def get_db_snapshot(self, snapshotid): - try: - snapshots = self.connection.describe_db_snapshots( - db_snapshot_identifier=snapshotid, - snapshot_type='manual' - )['DescribeDBSnapshotsResponse']['DescribeDBSnapshotsResult']['DBSnapshots'] - result = RDS2Snapshot(snapshots[0]) - return result - except boto.rds2.exceptions.DBSnapshotNotFound: - return None - - def create_db_instance(self, instance_name, size, instance_class, db_engine, - username, password, **params): - try: - result = self.connection.create_db_instance(instance_name, size, instance_class, db_engine, username, password, - **params)['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def create_db_instance_read_replica(self, instance_name, source_instance, **params): - try: - result = self.connection.create_db_instance_read_replica( - instance_name, - source_instance, - **params - )['CreateDBInstanceReadReplicaResponse']['CreateDBInstanceReadReplicaResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def delete_db_instance(self, instance_name, **params): - try: - result = self.connection.delete_db_instance(instance_name, **params)['DeleteDBInstanceResponse']['DeleteDBInstanceResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def delete_db_snapshot(self, snapshot): - try: - result = self.connection.delete_db_snapshot(snapshot)['DeleteDBSnapshotResponse']['DeleteDBSnapshotResult']['DBSnapshot'] - return RDS2Snapshot(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def modify_db_instance(self, instance_name, **params): - try: - result = self.connection.modify_db_instance(instance_name, **params)['ModifyDBInstanceResponse']['ModifyDBInstanceResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def reboot_db_instance(self, instance_name, **params): - try: - result = self.connection.reboot_db_instance(instance_name, **params)['RebootDBInstanceResponse']['RebootDBInstanceResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params): - try: - result = self.connection.restore_db_instance_from_db_snapshot( - instance_name, - snapshot, - **params - )['RestoreDBInstanceFromDBSnapshotResponse']['RestoreDBInstanceFromDBSnapshotResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def create_db_snapshot(self, snapshot, instance_name, **params): - try: - result = self.connection.create_db_snapshot(snapshot, instance_name, **params)['CreateDBSnapshotResponse']['CreateDBSnapshotResult']['DBSnapshot'] - return RDS2Snapshot(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - def promote_read_replica(self, instance_name, **params): - try: - result = self.connection.promote_read_replica(instance_name, **params)['PromoteReadReplicaResponse']['PromoteReadReplicaResult']['DBInstance'] - return RDS2DBInstance(result) - except boto.exception.BotoServerError as e: - raise RDSException(e) - - -class RDSDBInstance: - def __init__(self, dbinstance): - self.instance = dbinstance - self.name = dbinstance.id - self.status = dbinstance.status - - def get_data(self): - d = { - 'id': self.name, - 'create_time': self.instance.create_time, - 'status': self.status, - 'availability_zone': self.instance.availability_zone, - 'backup_retention': self.instance.backup_retention_period, - 'backup_window': self.instance.preferred_backup_window, - 'maintenance_window': self.instance.preferred_maintenance_window, - 'multi_zone': self.instance.multi_az, - 'instance_type': self.instance.instance_class, - 'username': self.instance.master_username, - 'iops': self.instance.iops - } - - # Only assign an Endpoint if one is available - if hasattr(self.instance, 'endpoint'): - d["endpoint"] = self.instance.endpoint[0] - d["port"] = self.instance.endpoint[1] - if self.instance.vpc_security_groups is not None: - d["vpc_security_groups"] = ','.join(x.vpc_group for x in self.instance.vpc_security_groups) - else: - d["vpc_security_groups"] = None - else: - d["endpoint"] = None - d["port"] = None - d["vpc_security_groups"] = None - d['DBName'] = self.instance.DBName if hasattr(self.instance, 'DBName') else None - # ReadReplicaSourceDBInstanceIdentifier may or may not exist - try: - d["replication_source"] = self.instance.ReadReplicaSourceDBInstanceIdentifier - except Exception: - d["replication_source"] = None - return d - - -class RDS2DBInstance: - def __init__(self, dbinstance): - self.instance = dbinstance - if 'DBInstanceIdentifier' not in dbinstance: - self.name = None - else: - self.name = self.instance.get('DBInstanceIdentifier') - self.status = self.instance.get('DBInstanceStatus') - - def get_data(self): - d = { - 'id': self.name, - 'create_time': self.instance['InstanceCreateTime'], - 'engine': self.instance['Engine'], - 'engine_version': self.instance['EngineVersion'], - 'license_model': self.instance['LicenseModel'], - 'character_set_name': self.instance['CharacterSetName'], - 'allocated_storage': self.instance['AllocatedStorage'], - 'publicly_accessible': self.instance['PubliclyAccessible'], - 'latest_restorable_time': self.instance['LatestRestorableTime'], - 'status': self.status, - 'availability_zone': self.instance['AvailabilityZone'], - 'secondary_availability_zone': self.instance['SecondaryAvailabilityZone'], - 'backup_retention': self.instance['BackupRetentionPeriod'], - 'backup_window': self.instance['PreferredBackupWindow'], - 'maintenance_window': self.instance['PreferredMaintenanceWindow'], - 'auto_minor_version_upgrade': self.instance['AutoMinorVersionUpgrade'], - 'read_replica_source_dbinstance_identifier': self.instance['ReadReplicaSourceDBInstanceIdentifier'], - 'multi_zone': self.instance['MultiAZ'], - 'instance_type': self.instance['DBInstanceClass'], - 'username': self.instance['MasterUsername'], - 'db_name': self.instance['DBName'], - 'iops': self.instance['Iops'], - 'replication_source': self.instance['ReadReplicaSourceDBInstanceIdentifier'] - } - if self.instance['DBParameterGroups'] is not None: - parameter_groups = [] - for x in self.instance['DBParameterGroups']: - parameter_groups.append({'parameter_group_name': x['DBParameterGroupName'], 'parameter_apply_status': x['ParameterApplyStatus']}) - d['parameter_groups'] = parameter_groups - if self.instance['OptionGroupMemberships'] is not None: - option_groups = [] - for x in self.instance['OptionGroupMemberships']: - option_groups.append({'status': x['Status'], 'option_group_name': x['OptionGroupName']}) - d['option_groups'] = option_groups - if self.instance['PendingModifiedValues'] is not None: - pdv = self.instance['PendingModifiedValues'] - d['pending_modified_values'] = { - 'multi_az': pdv['MultiAZ'], - 'master_user_password': pdv['MasterUserPassword'], - 'port': pdv['Port'], - 'iops': pdv['Iops'], - 'allocated_storage': pdv['AllocatedStorage'], - 'engine_version': pdv['EngineVersion'], - 'backup_retention_period': pdv['BackupRetentionPeriod'], - 'db_instance_class': pdv['DBInstanceClass'], - 'db_instance_identifier': pdv['DBInstanceIdentifier'] - } - if self.instance["DBSubnetGroup"] is not None: - dsg = self.instance["DBSubnetGroup"] - db_subnet_groups = {} - db_subnet_groups['vpc_id'] = dsg['VpcId'] - db_subnet_groups['name'] = dsg['DBSubnetGroupName'] - db_subnet_groups['status'] = dsg['SubnetGroupStatus'].lower() - db_subnet_groups['description'] = dsg['DBSubnetGroupDescription'] - db_subnet_groups['subnets'] = [] - for x in dsg["Subnets"]: - db_subnet_groups['subnets'].append({ - 'status': x['SubnetStatus'].lower(), - 'identifier': x['SubnetIdentifier'], - 'availability_zone': { - 'name': x['SubnetAvailabilityZone']['Name'], - 'provisioned_iops_capable': x['SubnetAvailabilityZone']['ProvisionedIopsCapable'] - } - }) - d['db_subnet_groups'] = db_subnet_groups - if self.instance["VpcSecurityGroups"] is not None: - d['vpc_security_groups'] = ','.join(x['VpcSecurityGroupId'] for x in self.instance['VpcSecurityGroups']) - if "Endpoint" in self.instance and self.instance["Endpoint"] is not None: - d['endpoint'] = self.instance["Endpoint"].get('Address', None) - d['port'] = self.instance["Endpoint"].get('Port', None) - else: - d['endpoint'] = None - d['port'] = None - d['DBName'] = self.instance['DBName'] if hasattr(self.instance, 'DBName') else None - return d - - -class RDSSnapshot: - def __init__(self, snapshot): - self.snapshot = snapshot - self.name = snapshot.id - self.status = snapshot.status - - def get_data(self): - d = { - 'id': self.name, - 'create_time': self.snapshot.snapshot_create_time, - 'status': self.status, - 'availability_zone': self.snapshot.availability_zone, - 'instance_id': self.snapshot.instance_id, - 'instance_created': self.snapshot.instance_create_time, - } - # needs boto >= 2.21.0 - if hasattr(self.snapshot, 'snapshot_type'): - d["snapshot_type"] = self.snapshot.snapshot_type - if hasattr(self.snapshot, 'iops'): - d["iops"] = self.snapshot.iops - return d - - -class RDS2Snapshot: - def __init__(self, snapshot): - if 'DeleteDBSnapshotResponse' in snapshot: - self.snapshot = snapshot['DeleteDBSnapshotResponse']['DeleteDBSnapshotResult']['DBSnapshot'] - else: - self.snapshot = snapshot - self.name = self.snapshot.get('DBSnapshotIdentifier') - self.status = self.snapshot.get('Status') - - def get_data(self): - d = { - 'id': self.name, - 'create_time': self.snapshot['SnapshotCreateTime'], - 'status': self.status, - 'availability_zone': self.snapshot['AvailabilityZone'], - 'instance_id': self.snapshot['DBInstanceIdentifier'], - 'instance_created': self.snapshot['InstanceCreateTime'], - 'snapshot_type': self.snapshot['SnapshotType'], - 'iops': self.snapshot['Iops'], - } - return d - - -def await_resource(conn, resource, status, module): - start_time = time.time() - wait_timeout = module.params.get('wait_timeout') + start_time - check_interval = 5 - while wait_timeout > time.time() and resource.status != status: - time.sleep(check_interval) - if wait_timeout <= time.time(): - module.fail_json(msg="Timeout waiting for RDS resource %s" % resource.name) - if module.params.get('command') == 'snapshot': - # Temporary until all the rds2 commands have their responses parsed - if resource.name is None: - module.fail_json(msg="There was a problem waiting for RDS snapshot %s" % resource.snapshot) - # Back off if we're getting throttled, since we're just waiting anyway - resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_snapshot)(resource.name) - else: - # Temporary until all the rds2 commands have their responses parsed - if resource.name is None: - module.fail_json(msg="There was a problem waiting for RDS instance %s" % resource.instance) - # Back off if we're getting throttled, since we're just waiting anyway - resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_instance)(resource.name) - if resource is None: - break - # Some RDS resources take much longer than others to be ready. Check - # less aggressively for slow ones to avoid throttling. - if time.time() > start_time + 90: - check_interval = 20 - return resource - - -def create_db_instance(module, conn): - required_vars = ['instance_name', 'db_engine', 'size', 'instance_type', 'username', 'password'] - valid_vars = ['backup_retention', 'backup_window', - 'character_set_name', 'db_name', 'engine_version', - 'instance_type', 'iops', 'license_model', 'maint_window', - 'multi_zone', 'option_group', 'parameter_group', 'port', - 'subnet', 'upgrade', 'zone'] - if module.params.get('subnet'): - valid_vars.append('vpc_security_groups') - else: - valid_vars.append('security_groups') - if HAS_RDS2: - valid_vars.extend(['publicly_accessible', 'tags']) - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - - result = conn.get_db_instance(instance_name) - if result: - changed = False - else: - try: - result = conn.create_db_instance(instance_name, module.params.get('size'), - module.params.get('instance_type'), module.params.get('db_engine'), - module.params.get('username'), module.params.get('password'), **params) - changed = True - except RDSException as e: - module.fail_json(msg="Failed to create instance: %s" % e.message) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - module.exit_json(changed=changed, instance=resource.get_data()) - - -def replicate_db_instance(module, conn): - required_vars = ['instance_name', 'source_instance'] - valid_vars = ['instance_type', 'port', 'upgrade', 'zone'] - if HAS_RDS2: - valid_vars.extend(['iops', 'option_group', 'publicly_accessible', 'tags']) - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - source_instance = module.params.get('source_instance') - - result = conn.get_db_instance(instance_name) - if result: - changed = False - else: - try: - result = conn.create_db_instance_read_replica(instance_name, source_instance, **params) - changed = True - except RDSException as e: - module.fail_json(msg="Failed to create replica instance: %s " % e.message) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - module.exit_json(changed=changed, instance=resource.get_data()) - - -def delete_db_instance_or_snapshot(module, conn): - required_vars = [] - valid_vars = ['instance_name', 'snapshot', 'skip_final_snapshot'] - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - snapshot = module.params.get('snapshot') - - if not instance_name: - result = conn.get_db_snapshot(snapshot) - else: - result = conn.get_db_instance(instance_name) - if not result: - module.exit_json(changed=False) - if result.status == 'deleting': - module.exit_json(changed=False) - try: - if instance_name: - if snapshot: - params["skip_final_snapshot"] = False - if HAS_RDS2: - params["final_db_snapshot_identifier"] = snapshot - else: - params["final_snapshot_id"] = snapshot - else: - params["skip_final_snapshot"] = True - result = conn.delete_db_instance(instance_name, **params) - else: - result = conn.delete_db_snapshot(snapshot) - except RDSException as e: - module.fail_json(msg="Failed to delete instance: %s" % e.message) - - # If we're not waiting for a delete to complete then we're all done - # so just return - if not module.params.get('wait'): - module.exit_json(changed=True) - try: - await_resource(conn, result, 'deleted', module) - module.exit_json(changed=True) - except RDSException as e: - if e.code == 'DBInstanceNotFound': - module.exit_json(changed=True) - else: - module.fail_json(msg=e.message) - except Exception as e: - module.fail_json(msg=str(e)) - - -def facts_db_instance_or_snapshot(module, conn): - instance_name = module.params.get('instance_name') - snapshot = module.params.get('snapshot') - - if instance_name and snapshot: - module.fail_json(msg="Facts must be called with either instance_name or snapshot, not both") - if instance_name: - resource = conn.get_db_instance(instance_name) - if not resource: - module.fail_json(msg="DB instance %s does not exist" % instance_name) - if snapshot: - resource = conn.get_db_snapshot(snapshot) - if not resource: - module.fail_json(msg="DB snapshot %s does not exist" % snapshot) - - module.exit_json(changed=False, instance=resource.get_data()) - - -def modify_db_instance(module, conn): - required_vars = ['instance_name'] - valid_vars = ['apply_immediately', 'backup_retention', 'backup_window', - 'db_name', 'engine_version', 'instance_type', 'iops', 'license_model', - 'maint_window', 'multi_zone', 'new_instance_name', - 'option_group', 'parameter_group', 'password', 'size', 'upgrade'] - - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - new_instance_name = module.params.get('new_instance_name') - - try: - result = conn.modify_db_instance(instance_name, **params) - except RDSException as e: - module.fail_json(msg=e.message) - if params.get('apply_immediately'): - if new_instance_name: - # Wait until the new instance name is valid - new_instance = None - while not new_instance: - new_instance = conn.get_db_instance(new_instance_name) - time.sleep(5) - - # Found instance but it briefly flicks to available - # before rebooting so let's wait until we see it rebooting - # before we check whether to 'wait' - result = await_resource(conn, new_instance, 'rebooting', module) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - # guess that this changed the DB, need a way to check - module.exit_json(changed=True, instance=resource.get_data()) - - -def promote_db_instance(module, conn): - required_vars = ['instance_name'] - valid_vars = ['backup_retention', 'backup_window'] - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - - result = conn.get_db_instance(instance_name) - if not result: - module.fail_json(msg="DB Instance %s does not exist" % instance_name) - - if result.get_data().get('replication_source'): - try: - result = conn.promote_read_replica(instance_name, **params) - changed = True - except RDSException as e: - module.fail_json(msg=e.message) - else: - changed = False - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - module.exit_json(changed=changed, instance=resource.get_data()) - - -def snapshot_db_instance(module, conn): - required_vars = ['instance_name', 'snapshot'] - valid_vars = ['tags'] - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - snapshot = module.params.get('snapshot') - changed = False - result = conn.get_db_snapshot(snapshot) - if not result: - try: - result = conn.create_db_snapshot(snapshot, instance_name, **params) - changed = True - except RDSException as e: - module.fail_json(msg=e.message) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_snapshot(snapshot) - - module.exit_json(changed=changed, snapshot=resource.get_data()) - - -def reboot_db_instance(module, conn): - required_vars = ['instance_name'] - valid_vars = [] - - if HAS_RDS2: - valid_vars.append('force_failover') - - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - result = conn.get_db_instance(instance_name) - changed = False - try: - result = conn.reboot_db_instance(instance_name, **params) - changed = True - except RDSException as e: - module.fail_json(msg=e.message) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - module.exit_json(changed=changed, instance=resource.get_data()) - - -def restore_db_instance(module, conn): - required_vars = ['instance_name', 'snapshot'] - valid_vars = ['db_name', 'iops', 'license_model', 'multi_zone', - 'option_group', 'port', 'publicly_accessible', - 'subnet', 'tags', 'upgrade', 'zone'] - if HAS_RDS2: - valid_vars.append('instance_type') - else: - required_vars.append('instance_type') - params = validate_parameters(required_vars, valid_vars, module) - instance_name = module.params.get('instance_name') - instance_type = module.params.get('instance_type') - snapshot = module.params.get('snapshot') - - changed = False - result = conn.get_db_instance(instance_name) - if not result: - try: - result = conn.restore_db_instance_from_db_snapshot(instance_name, snapshot, instance_type, **params) - changed = True - except RDSException as e: - module.fail_json(msg=e.message) - - if module.params.get('wait'): - resource = await_resource(conn, result, 'available', module) - else: - resource = conn.get_db_instance(instance_name) - - module.exit_json(changed=changed, instance=resource.get_data()) - - -def validate_parameters(required_vars, valid_vars, module): - command = module.params.get('command') - for v in required_vars: - if not module.params.get(v): - module.fail_json(msg="Parameter %s required for %s command" % (v, command)) - - # map to convert rds module options to boto rds and rds2 options - optional_params = { - 'port': 'port', - 'db_name': 'db_name', - 'zone': 'availability_zone', - 'maint_window': 'preferred_maintenance_window', - 'backup_window': 'preferred_backup_window', - 'backup_retention': 'backup_retention_period', - 'multi_zone': 'multi_az', - 'engine_version': 'engine_version', - 'upgrade': 'auto_minor_version_upgrade', - 'subnet': 'db_subnet_group_name', - 'license_model': 'license_model', - 'option_group': 'option_group_name', - 'size': 'allocated_storage', - 'iops': 'iops', - 'new_instance_name': 'new_instance_id', - 'apply_immediately': 'apply_immediately', - } - # map to convert rds module options to boto rds options - optional_params_rds = { - 'db_engine': 'engine', - 'password': 'master_password', - 'parameter_group': 'param_group', - 'instance_type': 'instance_class', - } - # map to convert rds module options to boto rds2 options - optional_params_rds2 = { - 'tags': 'tags', - 'publicly_accessible': 'publicly_accessible', - 'parameter_group': 'db_parameter_group_name', - 'character_set_name': 'character_set_name', - 'instance_type': 'db_instance_class', - 'password': 'master_user_password', - 'new_instance_name': 'new_db_instance_identifier', - 'force_failover': 'force_failover', - } - if HAS_RDS2: - optional_params.update(optional_params_rds2) - sec_group = 'db_security_groups' - else: - optional_params.update(optional_params_rds) - sec_group = 'security_groups' - # Check for options only supported with rds2 - for k in set(optional_params_rds2.keys()) - set(optional_params_rds.keys()): - if module.params.get(k): - module.fail_json(msg="Parameter %s requires boto.rds (boto >= 2.26.0)" % k) - - params = {} - for (k, v) in optional_params.items(): - if module.params.get(k) is not None and k not in required_vars: - if k in valid_vars: - params[v] = module.params[k] - else: - if module.params.get(k) is False: - pass - else: - module.fail_json(msg="Parameter %s is not valid for %s command" % (k, command)) - - if module.params.get('security_groups'): - params[sec_group] = module.params.get('security_groups').split(',') - - vpc_groups = module.params.get('vpc_security_groups') - if vpc_groups: - if HAS_RDS2: - params['vpc_security_group_ids'] = vpc_groups - else: - groups_list = [] - for x in vpc_groups: - groups_list.append(boto.rds.VPCSecurityGroupMembership(vpc_group=x)) - params['vpc_security_groups'] = groups_list - - # Convert tags dict to list of tuples that rds2 expects - if 'tags' in params: - params['tags'] = module.params['tags'].items() - return params - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - command=dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'reboot', 'restore'], required=True), - instance_name=dict(required=False), - source_instance=dict(required=False), - db_engine=dict(choices=['mariadb', 'MySQL', 'oracle-se1', 'oracle-se2', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', - 'sqlserver-web', 'postgres', 'aurora'], required=False), - size=dict(required=False), - instance_type=dict(aliases=['type'], required=False), - username=dict(required=False), - password=dict(no_log=True, required=False), - db_name=dict(required=False), - engine_version=dict(required=False), - parameter_group=dict(required=False), - license_model=dict(choices=['license-included', 'bring-your-own-license', 'general-public-license', 'postgresql-license'], required=False), - multi_zone=dict(type='bool', required=False), - iops=dict(required=False), - security_groups=dict(required=False), - vpc_security_groups=dict(type='list', required=False), - port=dict(required=False, type='int'), - upgrade=dict(type='bool', default=False), - option_group=dict(required=False), - maint_window=dict(required=False), - backup_window=dict(required=False), - backup_retention=dict(required=False), - zone=dict(aliases=['aws_zone', 'ec2_zone'], required=False), - subnet=dict(required=False), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=300), - snapshot=dict(required=False), - apply_immediately=dict(type='bool', default=False), - new_instance_name=dict(required=False), - tags=dict(type='dict', required=False), - publicly_accessible=dict(required=False), - character_set_name=dict(required=False), - force_failover=dict(type='bool', required=False, default=False) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - invocations = { - 'create': create_db_instance, - 'replicate': replicate_db_instance, - 'delete': delete_db_instance_or_snapshot, - 'facts': facts_db_instance_or_snapshot, - 'modify': modify_db_instance, - 'promote': promote_db_instance, - 'snapshot': snapshot_db_instance, - 'reboot': reboot_db_instance, - 'restore': restore_db_instance, - } - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - - # set port to per db defaults if not specified - if module.params['port'] is None and module.params['db_engine'] is not None and module.params['command'] == 'create': - if '-' in module.params['db_engine']: - engine = module.params['db_engine'].split('-')[0] - else: - engine = module.params['db_engine'] - module.params['port'] = DEFAULT_PORTS[engine.lower()] - - # connect to the rds endpoint - if HAS_RDS2: - conn = RDS2Connection(module, region, **aws_connect_params) - else: - conn = RDSConnection(module, region, **aws_connect_params) - - invocations[module.params.get('command')](module, conn) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_instance.py b/lib/ansible/modules/cloud/amazon/rds_instance.py deleted file mode 100644 index 8515aa0735..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_instance.py +++ /dev/null @@ -1,1226 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2018 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: rds_instance -version_added: "2.7" -short_description: Manage RDS instances -description: - - Create, modify, and delete RDS instances. - -requirements: - - botocore - - boto3 >= 1.5.0 -extends_documentation_fragment: - - aws - - ec2 -author: - - Sloane Hertel (@s-hertel) - -options: - # General module options - state: - description: - - Whether the snapshot should exist or not. I(rebooted) is not idempotent and will leave the DB instance in a running state - and start it prior to rebooting if it was stopped. I(present) will leave the DB instance in the current running/stopped state, - (running if creating the DB instance). - - I(state=running) and I(state=started) are synonyms, as are I(state=rebooted) and I(state=restarted). Note - rebooting the instance - is not idempotent. - choices: ['present', 'absent', 'terminated', 'running', 'started', 'stopped', 'rebooted', 'restarted'] - default: 'present' - type: str - creation_source: - description: Which source to use if restoring from a template (an existing instance, S3 bucket, or snapshot). - choices: ['snapshot', 's3', 'instance'] - type: str - force_update_password: - description: - - Set to True to update your cluster password with I(master_user_password). Since comparing passwords to determine - if it needs to be updated is not possible this is set to False by default to allow idempotence. - type: bool - default: False - purge_cloudwatch_logs_exports: - description: Set to False to retain any enabled cloudwatch logs that aren't specified in the task and are associated with the instance. - type: bool - default: True - purge_tags: - description: Set to False to retain any tags that aren't specified in task and are associated with the instance. - type: bool - default: True - read_replica: - description: - - Set to False to promote a read replica cluster or true to create one. When creating a read replica C(creation_source) should - be set to 'instance' or not provided. C(source_db_instance_identifier) must be provided with this option. - type: bool - wait: - description: - - Whether to wait for the cluster to be available, stopped, or deleted. At a later time a wait_timeout option may be added. - Following each API call to create/modify/delete the instance a waiter is used with a 60 second delay 30 times until the instance reaches - the expected state (available/stopped/deleted). The total task time may also be influenced by AWSRetry which helps stabilize if the - instance is in an invalid state to operate on to begin with (such as if you try to stop it when it is in the process of rebooting). - If setting this to False task retries and delays may make your playbook execution better handle timeouts for major modifications. - type: bool - default: True - - # Options that have a corresponding boto3 parameter - allocated_storage: - description: - - The amount of storage (in gibibytes) to allocate for the DB instance. - type: int - allow_major_version_upgrade: - description: - - Whether to allow major version upgrades. - type: bool - apply_immediately: - description: - - A value that specifies whether modifying a cluster with I(new_db_instance_identifier) and I(master_user_password) - should be applied as soon as possible, regardless of the I(preferred_maintenance_window) setting. If false, changes - are applied during the next maintenance window. - type: bool - default: False - auto_minor_version_upgrade: - description: - - Whether minor version upgrades are applied automatically to the DB instance during the maintenance window. - type: bool - availability_zone: - description: - - A list of EC2 Availability Zones that instances in the DB cluster can be created in. - May be used when creating a cluster or when restoring from S3 or a snapshot. Mutually exclusive with I(multi_az). - aliases: - - az - - zone - type: str - backup_retention_period: - description: - - The number of days for which automated backups are retained (must be greater or equal to 1). - May be used when creating a new cluster, when restoring from S3, or when modifying a cluster. - type: int - ca_certificate_identifier: - description: - - The identifier of the CA certificate for the DB instance. - type: str - character_set_name: - description: - - The character set to associate with the DB cluster. - type: str - copy_tags_to_snapshot: - description: - - Whether or not to copy all tags from the DB instance to snapshots of the instance. When initially creating - a DB instance the RDS API defaults this to false if unspecified. - type: bool - db_cluster_identifier: - description: - - The DB cluster (lowercase) identifier to add the aurora DB instance to. The identifier must contain from 1 to - 63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or - contain consecutive hyphens. - aliases: - - cluster_id - type: str - db_instance_class: - description: - - The compute and memory capacity of the DB instance, for example db.t2.micro. - aliases: - - class - - instance_type - type: str - db_instance_identifier: - description: - - The DB instance (lowercase) identifier. The identifier must contain from 1 to 63 letters, numbers, or - hyphens and the first character must be a letter and may not end in a hyphen or contain consecutive hyphens. - aliases: - - instance_id - - id - required: True - type: str - db_name: - description: - - The name for your database. If a name is not provided Amazon RDS will not create a database. - type: str - db_parameter_group_name: - description: - - The name of the DB parameter group to associate with this DB instance. When creating the DB instance if this - argument is omitted the default DBParameterGroup for the specified engine is used. - type: str - db_security_groups: - description: - - (EC2-Classic platform) A list of DB security groups to associate with this DB instance. - type: list - db_snapshot_identifier: - description: - - The identifier for the DB snapshot to restore from if using I(creation_source=snapshot). - type: str - db_subnet_group_name: - description: - - The DB subnet group name to use for the DB instance. - aliases: - - subnet_group - type: str - domain: - description: - - The Active Directory Domain to restore the instance in. - type: str - domain_iam_role_name: - description: - - The name of the IAM role to be used when making API calls to the Directory Service. - type: str - enable_cloudwatch_logs_exports: - description: - - A list of log types that need to be enabled for exporting to CloudWatch Logs. - aliases: - - cloudwatch_log_exports - type: list - enable_iam_database_authentication: - description: - - Enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts. - If this option is omitted when creating the cluster, Amazon RDS sets this to False. - type: bool - enable_performance_insights: - description: - - Whether to enable Performance Insights for the DB instance. - type: bool - engine: - description: - - The name of the database engine to be used for this DB instance. This is required to create an instance. - Valid choices are aurora | aurora-mysql | aurora-postgresql | mariadb | mysql | oracle-ee | oracle-se | - oracle-se1 | oracle-se2 | postgres | sqlserver-ee | sqlserver-ex | sqlserver-se | sqlserver-web - type: str - engine_version: - description: - - The version number of the database engine to use. For Aurora MySQL that could be 5.6.10a , 5.7.12. - Aurora PostgreSQL example, 9.6.3 - type: str - final_db_snapshot_identifier: - description: - - The DB instance snapshot identifier of the new DB instance snapshot created when I(skip_final_snapshot) is false. - aliases: - - final_snapshot_identifier - type: str - force_failover: - description: - - Set to true to conduct the reboot through a MultiAZ failover. - type: bool - iops: - description: - - The Provisioned IOPS (I/O operations per second) value. Is only set when using I(storage_type) is set to io1. - type: int - kms_key_id: - description: - - The ARN of the AWS KMS key identifier for an encrypted DB instance. If you are creating a DB instance with the - same AWS account that owns the KMS encryption key used to encrypt the new DB instance, then you can use the KMS key - alias instead of the ARN for the KM encryption key. - - If I(storage_encrypted) is true and and this option is not provided, the default encryption key is used. - type: str - license_model: - description: - - The license model for the DB instance. - - Several options are license-included, bring-your-own-license, and general-public-license. - - This option can also be omitted to default to an accepted value. - type: str - master_user_password: - description: - - An 8-41 character password for the master database user. The password can contain any printable ASCII character - except "/", """, or "@". To modify the password use I(force_password_update). Use I(apply immediately) to change - the password immediately, otherwise it is updated during the next maintenance window. - aliases: - - password - type: str - master_username: - description: - - The name of the master user for the DB cluster. Must be 1-16 letters or numbers and begin with a letter. - aliases: - - username - type: str - max_allocated_storage: - description: - - The upper limit to which Amazon RDS can automatically scale the storage of the DB instance. - type: int - version_added: "2.9" - monitoring_interval: - description: - - The interval, in seconds, when Enhanced Monitoring metrics are collected for the DB instance. To disable collecting - metrics, specify 0. Amazon RDS defaults this to 0 if omitted when initially creating a DB instance. - type: int - monitoring_role_arn: - description: - - The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. - type: str - multi_az: - description: - - Specifies if the DB instance is a Multi-AZ deployment. Mutually exclusive with I(availability_zone). - type: bool - new_db_instance_identifier: - description: - - The new DB cluster (lowercase) identifier for the DB cluster when renaming a DB instance. The identifier must contain - from 1 to 63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or - contain consecutive hyphens. Use I(apply_immediately) to rename immediately, otherwise it is updated during the - next maintenance window. - aliases: - - new_instance_id - - new_id - type: str - option_group_name: - description: - - The option group to associate with the DB instance. - type: str - performance_insights_kms_key_id: - description: - - The AWS KMS key identifier (ARN, name, or alias) for encryption of Performance Insights data. - type: str - performance_insights_retention_period: - description: - - The amount of time, in days, to retain Performance Insights data. Valid values are 7 or 731. - type: int - port: - description: - - The port number on which the instances accept connections. - type: int - preferred_backup_window: - description: - - The daily time range (in UTC) of at least 30 minutes, during which automated backups are created if automated backups are - enabled using I(backup_retention_period). The option must be in the format of "hh24:mi-hh24:mi" and not conflict with - I(preferred_maintenance_window). - aliases: - - backup_window - type: str - preferred_maintenance_window: - description: - - The weekly time range (in UTC) of at least 30 minutes, during which system maintenance can occur. The option must - be in the format "ddd:hh24:mi-ddd:hh24:mi" where ddd is one of Mon, Tue, Wed, Thu, Fri, Sat, Sun. - aliases: - - maintenance_window - type: str - processor_features: - description: - - A dictionary of Name, Value pairs to indicate the number of CPU cores and the number of threads per core for the - DB instance class of the DB instance. Names are threadsPerCore and coreCount. - Set this option to an empty dictionary to use the default processor features. - suboptions: - threadsPerCore: - description: The number of threads per core - coreCount: - description: The number of CPU cores - type: dict - promotion_tier: - description: - - An integer that specifies the order in which an Aurora Replica is promoted to the primary instance after a failure of - the existing primary instance. - type: str - publicly_accessible: - description: - - Specifies the accessibility options for the DB instance. A value of true specifies an Internet-facing instance with - a publicly resolvable DNS name, which resolves to a public IP address. A value of false specifies an internal - instance with a DNS name that resolves to a private IP address. - type: bool - restore_time: - description: - - If using I(creation_source=instance) this indicates the UTC date and time to restore from the source instance. - For example, "2009-09-07T23:45:00Z". - - May alternatively set I(use_latest_restore_time=True). - - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided. - type: str - s3_bucket_name: - description: - - The name of the Amazon S3 bucket that contains the data used to create the Amazon DB instance. - type: str - s3_ingestion_role_arn: - description: - - The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that authorizes Amazon RDS to access - the Amazon S3 bucket on your behalf. - type: str - s3_prefix: - description: - - The prefix for all of the file names that contain the data used to create the Amazon DB instance. If you do not - specify a SourceS3Prefix value, then the Amazon DB instance is created by using all of the files in the Amazon S3 bucket. - type: str - skip_final_snapshot: - description: - - Whether a final DB cluster snapshot is created before the DB cluster is deleted. If this is false I(final_db_snapshot_identifier) - must be provided. - type: bool - default: false - snapshot_identifier: - description: - - The ARN of the DB snapshot to restore from when using I(creation_source=snapshot). - type: str - source_db_instance_identifier: - description: - - The identifier or ARN of the source DB instance from which to restore when creating a read replica or spinning up a point-in-time - DB instance using I(creation_source=instance). If the source DB is not in the same region this should be an ARN. - type: str - source_engine: - description: - - The identifier for the database engine that was backed up to create the files stored in the Amazon S3 bucket. - choices: - - mysql - type: str - source_engine_version: - description: - - The version of the database that the backup files were created from. - type: str - source_region: - description: - - The region of the DB instance from which the replica is created. - type: str - storage_encrypted: - description: - - Whether the DB instance is encrypted. - type: bool - storage_type: - description: - - The storage type to be associated with the DB instance. I(storage_type) does not apply to Aurora DB instances. - choices: - - standard - - gp2 - - io1 - type: str - tags: - description: - - A dictionary of key value pairs to assign the DB cluster. - type: dict - tde_credential_arn: - description: - - The ARN from the key store with which to associate the instance for Transparent Data Encryption. This is - supported by Oracle or SQL Server DB instances and may be used in conjunction with C(storage_encrypted) - though it might slightly affect the performance of your database. - aliases: - - transparent_data_encryption_arn - type: str - tde_credential_password: - description: - - The password for the given ARN from the key store in order to access the device. - aliases: - - transparent_data_encryption_password - type: str - timezone: - description: - - The time zone of the DB instance. - type: str - use_latest_restorable_time: - description: - - Whether to restore the DB instance to the latest restorable backup time. - - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided. - type: bool - aliases: - - restore_from_latest - vpc_security_group_ids: - description: - - A list of EC2 VPC security groups to associate with the DB cluster. - type: list -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. -- name: create minimal aurora instance in default VPC and default subnet group - rds_instance: - engine: aurora - db_instance_identifier: ansible-test-aurora-db-instance - instance_type: db.t2.small - password: "{{ password }}" - username: "{{ username }}" - cluster_id: ansible-test-cluster # This cluster must exist - see rds_cluster to manage it - -- name: Create a DB instance using the default AWS KMS encryption key - rds_instance: - id: test-encrypted-db - state: present - engine: mariadb - storage_encrypted: True - db_instance_class: db.t2.medium - username: "{{ username }}" - password: "{{ password }}" - allocated_storage: "{{ allocated_storage }}" - -- name: remove the DB instance without a final snapshot - rds_instance: - id: "{{ instance_id }}" - state: absent - skip_final_snapshot: True - -- name: remove the DB instance with a final snapshot - rds_instance: - id: "{{ instance_id }}" - state: absent - final_snapshot_identifier: "{{ snapshot_id }}" -''' - -RETURN = ''' -allocated_storage: - description: The allocated storage size in gibibytes. This is always 1 for aurora database engines. - returned: always - type: int - sample: 20 -auto_minor_version_upgrade: - description: Whether minor engine upgrades are applied automatically to the DB instance during the maintenance window. - returned: always - type: bool - sample: true -availability_zone: - description: The availability zone for the DB instance. - returned: always - type: str - sample: us-east-1f -backup_retention_period: - description: The number of days for which automated backups are retained. - returned: always - type: int - sample: 1 -ca_certificate_identifier: - description: The identifier of the CA certificate for the DB instance. - returned: always - type: str - sample: rds-ca-2015 -copy_tags_to_snapshot: - description: Whether tags are copied from the DB instance to snapshots of the DB instance. - returned: always - type: bool - sample: false -db_instance_arn: - description: The Amazon Resource Name (ARN) for the DB instance. - returned: always - type: str - sample: arn:aws:rds:us-east-1:123456789012:db:ansible-test -db_instance_class: - description: The name of the compute and memory capacity class of the DB instance. - returned: always - type: str - sample: db.m4.large -db_instance_identifier: - description: The identifier of the DB instance - returned: always - type: str - sample: ansible-test -db_instance_port: - description: The port that the DB instance listens on. - returned: always - type: int - sample: 0 -db_instance_status: - description: The current state of this database. - returned: always - type: str - sample: stopped -db_parameter_groups: - description: The list of DB parameter groups applied to this DB instance. - returned: always - type: complex - contains: - db_parameter_group_name: - description: The name of the DP parameter group. - returned: always - type: str - sample: default.mariadb10.0 - parameter_apply_status: - description: The status of parameter updates. - returned: always - type: str - sample: in-sync -db_security_groups: - description: A list of DB security groups associated with this DB instance. - returned: always - type: list - sample: [] -db_subnet_group: - description: The subnet group associated with the DB instance. - returned: always - type: complex - contains: - db_subnet_group_description: - description: The description of the DB subnet group. - returned: always - type: str - sample: default - db_subnet_group_name: - description: The name of the DB subnet group. - returned: always - type: str - sample: default - subnet_group_status: - description: The status of the DB subnet group. - returned: always - type: str - sample: Complete - subnets: - description: A list of Subnet elements. - returned: always - type: complex - contains: - subnet_availability_zone: - description: The availability zone of the subnet. - returned: always - type: complex - contains: - name: - description: The name of the Availability Zone. - returned: always - type: str - sample: us-east-1c - subnet_identifier: - description: The ID of the subnet. - returned: always - type: str - sample: subnet-12345678 - subnet_status: - description: The status of the subnet. - returned: always - type: str - sample: Active - vpc_id: - description: The VpcId of the DB subnet group. - returned: always - type: str - sample: vpc-12345678 -dbi_resource_id: - description: The AWS Region-unique, immutable identifier for the DB instance. - returned: always - type: str - sample: db-UHV3QRNWX4KB6GALCIGRML6QFA -domain_memberships: - description: The Active Directory Domain membership records associated with the DB instance. - returned: always - type: list - sample: [] -endpoint: - description: The connection endpoint. - returned: always - type: complex - contains: - address: - description: The DNS address of the DB instance. - returned: always - type: str - sample: ansible-test.cvlrtwiennww.us-east-1.rds.amazonaws.com - hosted_zone_id: - description: The ID that Amazon Route 53 assigns when you create a hosted zone. - returned: always - type: str - sample: ZTR2ITUGPA61AM - port: - description: The port that the database engine is listening on. - returned: always - type: int - sample: 3306 -engine: - description: The database engine version. - returned: always - type: str - sample: mariadb -engine_version: - description: The database engine version. - returned: always - type: str - sample: 10.0.35 -iam_database_authentication_enabled: - description: Whether mapping of AWS Identity and Access Management (IAM) accounts to database accounts is enabled. - returned: always - type: bool - sample: false -instance_create_time: - description: The date and time the DB instance was created. - returned: always - type: str - sample: '2018-07-04T16:48:35.332000+00:00' -kms_key_id: - description: The AWS KMS key identifier for the encrypted DB instance when storage_encrypted is true. - returned: When storage_encrypted is true - type: str - sample: arn:aws:kms:us-east-1:123456789012:key/70c45553-ad2e-4a85-9f14-cfeb47555c33 -latest_restorable_time: - description: The latest time to which a database can be restored with point-in-time restore. - returned: always - type: str - sample: '2018-07-04T16:50:50.642000+00:00' -license_model: - description: The License model information for this DB instance. - returned: always - type: str - sample: general-public-license -master_username: - description: The master username for the DB instance. - returned: always - type: str - sample: test -max_allocated_storage: - description: The upper limit to which Amazon RDS can automatically scale the storage of the DB instance. - returned: When max allocated storage is present. - type: int - sample: 100 -monitoring_interval: - description: - - The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. - 0 means collecting Enhanced Monitoring metrics is disabled. - returned: always - type: int - sample: 0 -multi_az: - description: Whether the DB instance is a Multi-AZ deployment. - returned: always - type: bool - sample: false -option_group_memberships: - description: The list of option group memberships for this DB instance. - returned: always - type: complex - contains: - option_group_name: - description: The name of the option group that the instance belongs to. - returned: always - type: str - sample: default:mariadb-10-0 - status: - description: The status of the DB instance's option group membership. - returned: always - type: str - sample: in-sync -pending_modified_values: - description: The changes to the DB instance that are pending. - returned: always - type: complex - contains: {} -performance_insights_enabled: - description: True if Performance Insights is enabled for the DB instance, and otherwise false. - returned: always - type: bool - sample: false -preferred_backup_window: - description: The daily time range during which automated backups are created if automated backups are enabled. - returned: always - type: str - sample: 07:01-07:31 -preferred_maintenance_window: - description: The weekly time range (in UTC) during which system maintenance can occur. - returned: always - type: str - sample: sun:09:31-sun:10:01 -publicly_accessible: - description: - - True for an Internet-facing instance with a publicly resolvable DNS name, False to indicate an - internal instance with a DNS name that resolves to a private IP address. - returned: always - type: bool - sample: true -read_replica_db_instance_identifiers: - description: Identifiers of the Read Replicas associated with this DB instance. - returned: always - type: list - sample: [] -storage_encrypted: - description: Whether the DB instance is encrypted. - returned: always - type: bool - sample: false -storage_type: - description: The storage type to be associated with the DB instance. - returned: always - type: str - sample: standard -tags: - description: A dictionary of tags associated with the DB instance. - returned: always - type: complex - contains: {} -vpc_security_groups: - description: A list of VPC security group elements that the DB instance belongs to. - returned: always - type: complex - contains: - status: - description: The status of the VPC security group. - returned: always - type: str - sample: active - vpc_security_group_id: - description: The name of the VPC security group. - returned: always - type: str - sample: sg-12345678 -''' - -from ansible.module_utils._text import to_text -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters -from ansible.module_utils.aws.rds import ensure_tags, arg_spec_to_rds_params, call_method, get_rds_method_attribute, get_tags, get_final_identifier -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, AWSRetry -from ansible.module_utils.six import string_types - -from time import sleep - -try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError -except ImportError: - pass # caught by AnsibleAWSModule - - -def get_rds_method_attribute_name(instance, state, creation_source, read_replica): - method_name = None - if state == 'absent' or state == 'terminated': - if instance and instance['DBInstanceStatus'] not in ['deleting', 'deleted']: - method_name = 'delete_db_instance' - else: - if instance: - method_name = 'modify_db_instance' - elif read_replica is True: - method_name = 'create_db_instance_read_replica' - elif creation_source == 'snapshot': - method_name = 'restore_db_instance_from_db_snapshot' - elif creation_source == 's3': - method_name = 'restore_db_instance_from_s3' - elif creation_source == 'instance': - method_name = 'restore_db_instance_to_point_in_time' - else: - method_name = 'create_db_instance' - return method_name - - -def get_instance(client, module, db_instance_id): - try: - for i in range(3): - try: - instance = client.describe_db_instances(DBInstanceIdentifier=db_instance_id)['DBInstances'][0] - instance['Tags'] = get_tags(client, module, instance['DBInstanceArn']) - if instance.get('ProcessorFeatures'): - instance['ProcessorFeatures'] = dict((feature['Name'], feature['Value']) for feature in instance['ProcessorFeatures']) - if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'): - instance['PendingModifiedValues']['ProcessorFeatures'] = dict( - (feature['Name'], feature['Value']) - for feature in instance['PendingModifiedValues']['ProcessorFeatures'] - ) - break - except is_boto3_error_code('DBInstanceNotFound'): - sleep(3) - else: - instance = {} - except (BotoCoreError, ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg='Failed to describe DB instances') - return instance - - -def get_final_snapshot(client, module, snapshot_identifier): - try: - snapshots = AWSRetry.jittered_backoff()(client.describe_db_snapshots)(DBSnapshotIdentifier=snapshot_identifier) - if len(snapshots.get('DBSnapshots', [])) == 1: - return snapshots['DBSnapshots'][0] - return {} - except is_boto3_error_code('DBSnapshotNotFound') as e: # May not be using wait: True - return {} - except (BotoCoreError, ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg='Failed to retrieve information about the final snapshot') - - -def get_parameters(client, module, parameters, method_name): - if method_name == 'restore_db_instance_to_point_in_time': - parameters['TargetDBInstanceIdentifier'] = module.params['db_instance_identifier'] - - required_options = get_boto3_client_method_parameters(client, method_name, required=True) - if any([parameters.get(k) is None for k in required_options]): - module.fail_json(msg='To {0} requires the parameters: {1}'.format( - get_rds_method_attribute(method_name, module).operation_description, required_options)) - options = get_boto3_client_method_parameters(client, method_name) - parameters = dict((k, v) for k, v in parameters.items() if k in options and v is not None) - - if parameters.get('ProcessorFeatures') is not None: - parameters['ProcessorFeatures'] = [{'Name': k, 'Value': to_text(v)} for k, v in parameters['ProcessorFeatures'].items()] - - # If this parameter is an empty list it can only be used with modify_db_instance (as the parameter UseDefaultProcessorFeatures) - if parameters.get('ProcessorFeatures') == [] and not method_name == 'modify_db_instance': - parameters.pop('ProcessorFeatures') - - if method_name == 'create_db_instance' and parameters.get('Tags'): - parameters['Tags'] = ansible_dict_to_boto3_tag_list(parameters['Tags']) - if method_name == 'modify_db_instance': - parameters = get_options_with_changing_values(client, module, parameters) - - return parameters - - -def get_options_with_changing_values(client, module, parameters): - instance_id = module.params['db_instance_identifier'] - purge_cloudwatch_logs = module.params['purge_cloudwatch_logs_exports'] - force_update_password = module.params['force_update_password'] - port = module.params['port'] - apply_immediately = parameters.pop('ApplyImmediately', None) - cloudwatch_logs_enabled = module.params['enable_cloudwatch_logs_exports'] - - if port: - parameters['DBPortNumber'] = port - if not force_update_password: - parameters.pop('MasterUserPassword', None) - if cloudwatch_logs_enabled: - parameters['CloudwatchLogsExportConfiguration'] = cloudwatch_logs_enabled - if not module.params['storage_type']: - parameters.pop('Iops', None) - - instance = get_instance(client, module, instance_id) - updated_parameters = get_changing_options_with_inconsistent_keys(parameters, instance, purge_cloudwatch_logs) - updated_parameters.update(get_changing_options_with_consistent_keys(parameters, instance)) - parameters = updated_parameters - - if parameters.get('NewDBInstanceIdentifier') and instance.get('PendingModifiedValues', {}).get('DBInstanceIdentifier'): - if parameters['NewDBInstanceIdentifier'] == instance['PendingModifiedValues']['DBInstanceIdentifier'] and not apply_immediately: - parameters.pop('NewDBInstanceIdentifier') - - if parameters: - parameters['DBInstanceIdentifier'] = instance_id - if apply_immediately is not None: - parameters['ApplyImmediately'] = apply_immediately - - return parameters - - -def get_current_attributes_with_inconsistent_keys(instance): - options = {} - if instance.get('PendingModifiedValues', {}).get('PendingCloudwatchLogsExports', {}).get('LogTypesToEnable', []): - current_enabled = instance['PendingModifiedValues']['PendingCloudwatchLogsExports']['LogTypesToEnable'] - current_disabled = instance['PendingModifiedValues']['PendingCloudwatchLogsExports']['LogTypesToDisable'] - options['CloudwatchLogsExportConfiguration'] = {'LogTypesToEnable': current_enabled, 'LogTypesToDisable': current_disabled} - else: - options['CloudwatchLogsExportConfiguration'] = {'LogTypesToEnable': instance.get('EnabledCloudwatchLogsExports', []), 'LogTypesToDisable': []} - if instance.get('PendingModifiedValues', {}).get('Port'): - options['DBPortNumber'] = instance['PendingModifiedValues']['Port'] - else: - options['DBPortNumber'] = instance['Endpoint']['Port'] - if instance.get('PendingModifiedValues', {}).get('DBSubnetGroupName'): - options['DBSubnetGroupName'] = instance['PendingModifiedValues']['DBSubnetGroupName'] - else: - options['DBSubnetGroupName'] = instance['DBSubnetGroup']['DBSubnetGroupName'] - if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'): - options['ProcessorFeatures'] = instance['PendingModifiedValues']['ProcessorFeatures'] - else: - options['ProcessorFeatures'] = instance.get('ProcessorFeatures', {}) - options['OptionGroupName'] = [g['OptionGroupName'] for g in instance['OptionGroupMemberships']] - options['DBSecurityGroups'] = [sg['DBSecurityGroupName'] for sg in instance['DBSecurityGroups'] if sg['Status'] in ['adding', 'active']] - options['VpcSecurityGroupIds'] = [sg['VpcSecurityGroupId'] for sg in instance['VpcSecurityGroups'] if sg['Status'] in ['adding', 'active']] - options['DBParameterGroupName'] = [parameter_group['DBParameterGroupName'] for parameter_group in instance['DBParameterGroups']] - options['AllowMajorVersionUpgrade'] = None - options['EnableIAMDatabaseAuthentication'] = instance['IAMDatabaseAuthenticationEnabled'] - # PerformanceInsightsEnabled is not returned on older RDS instances it seems - options['EnablePerformanceInsights'] = instance.get('PerformanceInsightsEnabled', False) - options['MasterUserPassword'] = None - options['NewDBInstanceIdentifier'] = instance['DBInstanceIdentifier'] - - return options - - -def get_changing_options_with_inconsistent_keys(modify_params, instance, purge_cloudwatch_logs): - changing_params = {} - current_options = get_current_attributes_with_inconsistent_keys(instance) - - if current_options.get("MaxAllocatedStorage") is None: - current_options["MaxAllocatedStorage"] = None - - for option in current_options: - current_option = current_options[option] - desired_option = modify_params.pop(option, None) - if desired_option is None: - continue - - # TODO: allow other purge_option module parameters rather than just checking for things to add - if isinstance(current_option, list): - if isinstance(desired_option, list): - if set(desired_option) <= set(current_option): - continue - elif isinstance(desired_option, string_types): - if desired_option in current_option: - continue - - if current_option == desired_option: - continue - - if option == 'ProcessorFeatures' and desired_option == []: - changing_params['UseDefaultProcessorFeatures'] = True - elif option == 'CloudwatchLogsExportConfiguration': - current_option = set(current_option.get('LogTypesToEnable', [])) - desired_option = set(desired_option) - format_option = {'EnableLogTypes': [], 'DisableLogTypes': []} - format_option['EnableLogTypes'] = list(desired_option.difference(current_option)) - if purge_cloudwatch_logs: - format_option['DisableLogTypes'] = list(current_option.difference(desired_option)) - if format_option['EnableLogTypes'] or format_option['DisableLogTypes']: - changing_params[option] = format_option - else: - changing_params[option] = desired_option - - return changing_params - - -def get_changing_options_with_consistent_keys(modify_params, instance): - inconsistent_parameters = list(modify_params.keys()) - changing_params = {} - - for param in modify_params: - current_option = instance.get('PendingModifiedValues', {}).get(param) - if current_option is None: - current_option = instance[param] - if modify_params[param] != current_option: - changing_params[param] = modify_params[param] - - return changing_params - - -def validate_options(client, module, instance): - state = module.params['state'] - skip_final_snapshot = module.params['skip_final_snapshot'] - snapshot_id = module.params['final_db_snapshot_identifier'] - modified_id = module.params['new_db_instance_identifier'] - engine = module.params['engine'] - tde_options = bool(module.params['tde_credential_password'] or module.params['tde_credential_arn']) - read_replica = module.params['read_replica'] - creation_source = module.params['creation_source'] - source_instance = module.params['source_db_instance_identifier'] - if module.params['source_region'] is not None: - same_region = bool(module.params['source_region'] == module.params['region']) - else: - same_region = True - - if modified_id: - modified_instance = get_instance(client, module, modified_id) - else: - modified_instance = {} - - if modified_id and instance and modified_instance: - module.fail_json(msg='A new instance ID {0} was provided but it already exists'.format(modified_id)) - if modified_id and not instance and modified_instance: - module.fail_json(msg='A new instance ID {0} was provided but the instance to be renamed does not exist'.format(modified_id)) - if state in ('absent', 'terminated') and instance and not skip_final_snapshot and snapshot_id is None: - module.fail_json(msg='skip_final_snapshot is false but all of the following are missing: final_db_snapshot_identifier') - if engine is not None and not (engine.startswith('mysql') or engine.startswith('oracle')) and tde_options: - module.fail_json(msg='TDE is available for MySQL and Oracle DB instances') - if read_replica is True and not instance and creation_source not in [None, 'instance']: - module.fail_json(msg='Cannot create a read replica from {0}. You must use a source DB instance'.format(creation_source)) - if read_replica is True and not instance and not source_instance: - module.fail_json(msg='read_replica is true and the instance does not exist yet but all of the following are missing: source_db_instance_identifier') - - -def update_instance(client, module, instance, instance_id): - changed = False - - # Get newly created DB instance - if not instance: - instance = get_instance(client, module, instance_id) - - # Check tagging/promoting/rebooting/starting/stopping instance - changed |= ensure_tags( - client, module, instance['DBInstanceArn'], instance['Tags'], module.params['tags'], module.params['purge_tags'] - ) - changed |= promote_replication_instance(client, module, instance, module.params['read_replica']) - changed |= update_instance_state(client, module, instance, module.params['state']) - - return changed - - -def promote_replication_instance(client, module, instance, read_replica): - changed = False - if read_replica is False: - changed = bool(instance.get('ReadReplicaSourceDBInstanceIdentifier') or instance.get('StatusInfos')) - if changed: - try: - call_method(client, module, method_name='promote_read_replica', parameters={'DBInstanceIdentifier': instance['DBInstanceIdentifier']}) - changed = True - except is_boto3_error_code('InvalidDBInstanceState') as e: - if 'DB Instance is not a read replica' in e.response['Error']['Message']: - pass - else: - raise e - return changed - - -def update_instance_state(client, module, instance, state): - changed = False - if state in ['rebooted', 'restarted']: - changed |= reboot_running_db_instance(client, module, instance) - if state in ['started', 'running', 'stopped']: - changed |= start_or_stop_instance(client, module, instance, state) - return changed - - -def reboot_running_db_instance(client, module, instance): - parameters = {'DBInstanceIdentifier': instance['DBInstanceIdentifier']} - if instance['DBInstanceStatus'] in ['stopped', 'stopping']: - call_method(client, module, 'start_db_instance', parameters) - if module.params.get('force_failover') is not None: - parameters['ForceFailover'] = module.params['force_failover'] - results, changed = call_method(client, module, 'reboot_db_instance', parameters) - return changed - - -def start_or_stop_instance(client, module, instance, state): - changed = False - parameters = {'DBInstanceIdentifier': instance['DBInstanceIdentifier']} - if state == 'stopped' and instance['DBInstanceStatus'] not in ['stopping', 'stopped']: - if module.params['db_snapshot_identifier']: - parameters['DBSnapshotIdentifier'] = module.params['db_snapshot_identifier'] - result, changed = call_method(client, module, 'stop_db_instance', parameters) - elif state == 'started' and instance['DBInstanceStatus'] not in ['available', 'starting', 'restarting']: - result, changed = call_method(client, module, 'start_db_instance', parameters) - return changed - - -def main(): - arg_spec = dict( - state=dict(choices=['present', 'absent', 'terminated', 'running', 'started', 'stopped', 'rebooted', 'restarted'], default='present'), - creation_source=dict(choices=['snapshot', 's3', 'instance']), - force_update_password=dict(type='bool', default=False), - purge_cloudwatch_logs_exports=dict(type='bool', default=True), - purge_tags=dict(type='bool', default=True), - read_replica=dict(type='bool'), - wait=dict(type='bool', default=True), - ) - - parameter_options = dict( - allocated_storage=dict(type='int'), - allow_major_version_upgrade=dict(type='bool'), - apply_immediately=dict(type='bool', default=False), - auto_minor_version_upgrade=dict(type='bool'), - availability_zone=dict(aliases=['az', 'zone']), - backup_retention_period=dict(type='int'), - ca_certificate_identifier=dict(), - character_set_name=dict(), - copy_tags_to_snapshot=dict(type='bool'), - db_cluster_identifier=dict(aliases=['cluster_id']), - db_instance_class=dict(aliases=['class', 'instance_type']), - db_instance_identifier=dict(required=True, aliases=['instance_id', 'id']), - db_name=dict(), - db_parameter_group_name=dict(), - db_security_groups=dict(type='list'), - db_snapshot_identifier=dict(), - db_subnet_group_name=dict(aliases=['subnet_group']), - domain=dict(), - domain_iam_role_name=dict(), - enable_cloudwatch_logs_exports=dict(type='list', aliases=['cloudwatch_log_exports']), - enable_iam_database_authentication=dict(type='bool'), - enable_performance_insights=dict(type='bool'), - engine=dict(), - engine_version=dict(), - final_db_snapshot_identifier=dict(aliases=['final_snapshot_identifier']), - force_failover=dict(type='bool'), - iops=dict(type='int'), - kms_key_id=dict(), - license_model=dict(), - master_user_password=dict(aliases=['password'], no_log=True), - master_username=dict(aliases=['username']), - max_allocated_storage=dict(type='int'), - monitoring_interval=dict(type='int'), - monitoring_role_arn=dict(), - multi_az=dict(type='bool'), - new_db_instance_identifier=dict(aliases=['new_instance_id', 'new_id']), - option_group_name=dict(), - performance_insights_kms_key_id=dict(), - performance_insights_retention_period=dict(type='int'), - port=dict(type='int'), - preferred_backup_window=dict(aliases=['backup_window']), - preferred_maintenance_window=dict(aliases=['maintenance_window']), - processor_features=dict(type='dict'), - promotion_tier=dict(), - publicly_accessible=dict(type='bool'), - restore_time=dict(), - s3_bucket_name=dict(), - s3_ingestion_role_arn=dict(), - s3_prefix=dict(), - skip_final_snapshot=dict(type='bool', default=False), - snapshot_identifier=dict(), - source_db_instance_identifier=dict(), - source_engine=dict(choices=['mysql']), - source_engine_version=dict(), - source_region=dict(), - storage_encrypted=dict(type='bool'), - storage_type=dict(choices=['standard', 'gp2', 'io1']), - tags=dict(type='dict'), - tde_credential_arn=dict(aliases=['transparent_data_encryption_arn']), - tde_credential_password=dict(no_log=True, aliases=['transparent_data_encryption_password']), - timezone=dict(), - use_latest_restorable_time=dict(type='bool', aliases=['restore_from_latest']), - vpc_security_group_ids=dict(type='list') - ) - arg_spec.update(parameter_options) - - required_if = [ - ('engine', 'aurora', ('db_cluster_identifier',)), - ('engine', 'aurora-mysql', ('db_cluster_identifier',)), - ('engine', 'aurora-postresql', ('db_cluster_identifier',)), - ('creation_source', 'snapshot', ('snapshot_identifier', 'engine')), - ('creation_source', 's3', ( - 's3_bucket_name', 'engine', 'master_username', 'master_user_password', - 'source_engine', 'source_engine_version', 's3_ingestion_role_arn')), - ] - mutually_exclusive = [ - ('s3_bucket_name', 'source_db_instance_identifier', 'snapshot_identifier'), - ('use_latest_restorable_time', 'restore_time'), - ('availability_zone', 'multi_az'), - ] - - module = AnsibleAWSModule( - argument_spec=arg_spec, - required_if=required_if, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True - ) - - if not module.boto3_at_least('1.5.0'): - module.fail_json(msg="rds_instance requires boto3 > 1.5.0") - - # Sanitize instance identifiers - module.params['db_instance_identifier'] = module.params['db_instance_identifier'].lower() - if module.params['new_db_instance_identifier']: - module.params['new_db_instance_identifier'] = module.params['new_db_instance_identifier'].lower() - - # Sanitize processor features - if module.params['processor_features'] is not None: - module.params['processor_features'] = dict((k, to_text(v)) for k, v in module.params['processor_features'].items()) - - client = module.client('rds') - changed = False - state = module.params['state'] - instance_id = module.params['db_instance_identifier'] - instance = get_instance(client, module, instance_id) - validate_options(client, module, instance) - method_name = get_rds_method_attribute_name(instance, state, module.params['creation_source'], module.params['read_replica']) - - if method_name: - raw_parameters = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in parameter_options)) - parameters = get_parameters(client, module, raw_parameters, method_name) - - if parameters: - result, changed = call_method(client, module, method_name, parameters) - - instance_id = get_final_identifier(method_name, module) - - # Check tagging/promoting/rebooting/starting/stopping instance - if state != 'absent' and (not module.check_mode or instance): - changed |= update_instance(client, module, instance, instance_id) - - if changed: - instance = get_instance(client, module, instance_id) - if state != 'absent' and (instance or not module.check_mode): - for attempt_to_wait in range(0, 10): - instance = get_instance(client, module, instance_id) - if instance: - break - else: - sleep(5) - - if state == 'absent' and changed and not module.params['skip_final_snapshot']: - instance.update(FinalSnapshot=get_final_snapshot(client, module, module.params['final_db_snapshot_identifier'])) - - pending_processor_features = None - if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'): - pending_processor_features = instance['PendingModifiedValues'].pop('ProcessorFeatures') - instance = camel_dict_to_snake_dict(instance, ignore_list=['Tags', 'ProcessorFeatures']) - if pending_processor_features is not None: - instance['pending_modified_values']['processor_features'] = pending_processor_features - - module.exit_json(changed=changed, **instance) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_instance_info.py b/lib/ansible/modules/cloud/amazon/rds_instance_info.py deleted file mode 100644 index 082fc84d35..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_instance_info.py +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017, 2018 Michael De La Rue -# Copyright (c) 2017, 2018 Will Thames -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: rds_instance_info -version_added: "2.6" -short_description: obtain information about one or more RDS instances -description: - - Obtain information about one or more RDS instances. - - This module was called C(rds_instance_facts) before Ansible 2.9. The usage did not change. -options: - db_instance_identifier: - description: - - The RDS instance's unique identifier. - required: false - aliases: - - id - type: str - filters: - description: - - A filter that specifies one or more DB instances to describe. - See U(https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBInstances.html) - type: dict -requirements: - - "python >= 2.7" - - "boto3" -author: - - "Will Thames (@willthames)" - - "Michael De La Rue (@mikedlr)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Get information about an instance -- rds_instance_info: - db_instance_identifier: new-database - register: new_database_info - -# Get all RDS instances -- rds_instance_info: -''' - -RETURN = ''' -instances: - description: List of RDS instances - returned: always - type: complex - contains: - allocated_storage: - description: Gigabytes of storage allocated to the database - returned: always - type: int - sample: 10 - auto_minor_version_upgrade: - description: Whether minor version upgrades happen automatically - returned: always - type: bool - sample: true - availability_zone: - description: Availability Zone in which the database resides - returned: always - type: str - sample: us-west-2b - backup_retention_period: - description: Days for which backups are retained - returned: always - type: int - sample: 7 - ca_certificate_identifier: - description: ID for the CA certificate - returned: always - type: str - sample: rds-ca-2015 - copy_tags_to_snapshot: - description: Whether DB tags should be copied to the snapshot - returned: always - type: bool - sample: false - db_instance_arn: - description: ARN of the database instance - returned: always - type: str - sample: arn:aws:rds:us-west-2:111111111111:db:helloworld-rds - db_instance_class: - description: Instance class of the database instance - returned: always - type: str - sample: db.t2.small - db_instance_identifier: - description: Database instance identifier - returned: always - type: str - sample: helloworld-rds - db_instance_port: - description: Port used by the database instance - returned: always - type: int - sample: 0 - db_instance_status: - description: Status of the database instance - returned: always - type: str - sample: available - db_name: - description: Name of the database - returned: always - type: str - sample: management - db_parameter_groups: - description: List of database parameter groups - returned: always - type: complex - contains: - db_parameter_group_name: - description: Name of the database parameter group - returned: always - type: str - sample: psql-pg-helloworld - parameter_apply_status: - description: Whether the parameter group has been applied - returned: always - type: str - sample: in-sync - db_security_groups: - description: List of security groups used by the database instance - returned: always - type: list - sample: [] - db_subnet_group: - description: list of subnet groups - returned: always - type: complex - contains: - db_subnet_group_description: - description: Description of the DB subnet group - returned: always - type: str - sample: My database subnet group - db_subnet_group_name: - description: Name of the database subnet group - returned: always - type: str - sample: my-subnet-group - subnet_group_status: - description: Subnet group status - returned: always - type: str - sample: Complete - subnets: - description: List of subnets in the subnet group - returned: always - type: complex - contains: - subnet_availability_zone: - description: Availability zone of the subnet - returned: always - type: complex - contains: - name: - description: Name of the availability zone - returned: always - type: str - sample: us-west-2c - subnet_identifier: - description: Subnet ID - returned: always - type: str - sample: subnet-abcd1234 - subnet_status: - description: Subnet status - returned: always - type: str - sample: Active - vpc_id: - description: VPC id of the subnet group - returned: always - type: str - sample: vpc-abcd1234 - dbi_resource_id: - description: AWS Region-unique, immutable identifier for the DB instance - returned: always - type: str - sample: db-AAAAAAAAAAAAAAAAAAAAAAAAAA - domain_memberships: - description: List of domain memberships - returned: always - type: list - sample: [] - endpoint: - description: Database endpoint - returned: always - type: complex - contains: - address: - description: Database endpoint address - returned: always - type: str - sample: helloworld-rds.ctrqpe3so1sf.us-west-2.rds.amazonaws.com - hosted_zone_id: - description: Route53 hosted zone ID - returned: always - type: str - sample: Z1PABCD0000000 - port: - description: Database endpoint port - returned: always - type: int - sample: 5432 - engine: - description: Database engine - returned: always - type: str - sample: postgres - engine_version: - description: Database engine version - returned: always - type: str - sample: 9.5.10 - iam_database_authentication_enabled: - description: Whether database authentication through IAM is enabled - returned: always - type: bool - sample: false - instance_create_time: - description: Date and time the instance was created - returned: always - type: str - sample: '2017-10-10T04:00:07.434000+00:00' - kms_key_id: - description: KMS Key ID - returned: always - type: str - sample: arn:aws:kms:us-west-2:111111111111:key/abcd1234-0000-abcd-1111-0123456789ab - latest_restorable_time: - description: Latest time to which a database can be restored with point-in-time restore - returned: always - type: str - sample: '2018-05-17T00:03:56+00:00' - license_model: - description: License model - returned: always - type: str - sample: postgresql-license - master_username: - description: Database master username - returned: always - type: str - sample: dbadmin - monitoring_interval: - description: Interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance - returned: always - type: int - sample: 0 - multi_az: - description: Whether Multi-AZ is on - returned: always - type: bool - sample: false - option_group_memberships: - description: List of option groups - returned: always - type: complex - contains: - option_group_name: - description: Option group name - returned: always - type: str - sample: default:postgres-9-5 - status: - description: Status of option group - returned: always - type: str - sample: in-sync - pending_modified_values: - description: Modified values pending application - returned: always - type: complex - contains: {} - performance_insights_enabled: - description: Whether performance insights are enabled - returned: always - type: bool - sample: false - preferred_backup_window: - description: Preferred backup window - returned: always - type: str - sample: 04:00-05:00 - preferred_maintenance_window: - description: Preferred maintenance window - returned: always - type: str - sample: mon:05:00-mon:05:30 - publicly_accessible: - description: Whether the DB is publicly accessible - returned: always - type: bool - sample: false - read_replica_db_instance_identifiers: - description: List of database instance read replicas - returned: always - type: list - sample: [] - storage_encrypted: - description: Whether the storage is encrypted - returned: always - type: bool - sample: true - storage_type: - description: Storage type of the Database instance - returned: always - type: str - sample: gp2 - tags: - description: Tags used by the database instance - returned: always - type: complex - contains: {} - vpc_security_groups: - description: List of VPC security groups - returned: always - type: complex - contains: - status: - description: Status of the VPC security group - returned: always - type: str - sample: active - vpc_security_group_id: - description: VPC Security Group ID - returned: always - type: str - sample: sg-abcd1234 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict, AWSRetry, camel_dict_to_snake_dict - - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - - -def instance_info(module, conn): - instance_name = module.params.get('db_instance_identifier') - filters = module.params.get('filters') - - params = dict() - if instance_name: - params['DBInstanceIdentifier'] = instance_name - if filters: - params['Filters'] = ansible_dict_to_boto3_filter_list(filters) - - paginator = conn.get_paginator('describe_db_instances') - try: - results = paginator.paginate(**params).build_full_result()['DBInstances'] - except is_boto3_error_code('DBInstanceNotFound'): - results = [] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, "Couldn't get instance information") - - for instance in results: - try: - instance['Tags'] = boto3_tag_list_to_ansible_dict(conn.list_tags_for_resource(ResourceName=instance['DBInstanceArn'], - aws_retry=True)['TagList']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Couldn't get tags for instance %s" % instance['DBInstanceIdentifier']) - - return dict(changed=False, instances=[camel_dict_to_snake_dict(instance, ignore_list=['Tags']) for instance in results]) - - -def main(): - argument_spec = dict( - db_instance_identifier=dict(aliases=['id']), - filters=dict(type='dict') - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - if module._name == 'rds_instance_facts': - module.deprecate("The 'rds_instance_facts' module has been renamed to 'rds_instance_info'", version='2.13') - - conn = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10)) - - module.exit_json(**instance_info(module, conn)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_param_group.py b/lib/ansible/modules/cloud/amazon/rds_param_group.py deleted file mode 100644 index 973fe20f91..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_param_group.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: rds_param_group -version_added: "1.5" -short_description: manage RDS parameter groups -description: - - Creates, modifies, and deletes RDS parameter groups. -requirements: [ boto3 ] -options: - state: - description: - - Specifies whether the group should be present or absent. - required: true - choices: [ 'present' , 'absent' ] - type: str - name: - description: - - Database parameter group identifier. - required: true - type: str - description: - description: - - Database parameter group description. Only set when a new group is added. - type: str - engine: - description: - - The type of database for this group. - - Please use following command to get list of all supported db engines and their respective versions. - - '# aws rds describe-db-engine-versions --query "DBEngineVersions[].DBParameterGroupFamily"' - - Required for I(state=present). - type: str - immediate: - description: - - Whether to apply the changes immediately, or after the next reboot of any associated instances. - aliases: - - apply_immediately - type: bool - params: - description: - - Map of parameter names and values. Numeric values may be represented as K for kilo (1024), M for mega (1024^2), G for giga (1024^3), - or T for tera (1024^4), and these values will be expanded into the appropriate number before being set in the parameter group. - aliases: [parameters] - type: dict - tags: - description: - - Dictionary of tags to attach to the parameter group. - version_added: "2.4" - type: dict - purge_tags: - description: - - Whether or not to remove tags that do not appear in the M(tags) list. - version_added: "2.4" - type: bool - default: False -author: - - "Scott Anderson (@tastychutney)" - - "Will Thames (@willthames)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Add or change a parameter group, in this case setting auto_increment_increment to 42 * 1024 -- rds_param_group: - state: present - name: norwegian-blue - description: 'My Fancy Ex Parrot Group' - engine: 'mysql5.6' - params: - auto_increment_increment: "42K" - tags: - Environment: production - Application: parrot - -# Remove a parameter group -- rds_param_group: - state: absent - name: norwegian-blue -''' - -RETURN = ''' -db_parameter_group_name: - description: Name of DB parameter group - type: str - returned: when state is present -db_parameter_group_family: - description: DB parameter group family that this DB parameter group is compatible with. - type: str - returned: when state is present -db_parameter_group_arn: - description: ARN of the DB parameter group - type: str - returned: when state is present -description: - description: description of the DB parameter group - type: str - returned: when state is present -errors: - description: list of errors from attempting to modify parameters that are not modifiable - type: list - returned: when state is present -tags: - description: dictionary of tags - type: dict - returned: when state is present -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, HAS_BOTO3, compare_aws_tags -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native - -import traceback - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -INT_MODIFIERS = { - 'K': 1024, - 'M': pow(1024, 2), - 'G': pow(1024, 3), - 'T': pow(1024, 4), -} - - -def convert_parameter(param, value): - """ - Allows setting parameters with 10M = 10* 1024 * 1024 and so on. - """ - converted_value = value - - if param['DataType'] == 'integer': - if isinstance(value, string_types): - try: - for modifier in INT_MODIFIERS.keys(): - if value.endswith(modifier): - converted_value = int(value[:-1]) * INT_MODIFIERS[modifier] - except ValueError: - # may be based on a variable (ie. {foo*3/4}) so - # just pass it on through to boto - pass - elif isinstance(value, bool): - converted_value = 1 if value else 0 - - elif param['DataType'] == 'boolean': - if isinstance(value, string_types): - converted_value = to_native(value) in BOOLEANS_TRUE - # convert True/False to 1/0 - converted_value = 1 if converted_value else 0 - return str(converted_value) - - -def update_parameters(module, connection): - groupname = module.params['name'] - desired = module.params['params'] - apply_method = 'immediate' if module.params['immediate'] else 'pending-reboot' - errors = [] - modify_list = [] - parameters_paginator = connection.get_paginator('describe_db_parameters') - existing = parameters_paginator.paginate(DBParameterGroupName=groupname).build_full_result()['Parameters'] - lookup = dict((param['ParameterName'], param) for param in existing) - for param_key, param_value in desired.items(): - if param_key not in lookup: - errors.append("Parameter %s is not an available parameter for the %s engine" % - (param_key, module.params.get('engine'))) - else: - converted_value = convert_parameter(lookup[param_key], param_value) - # engine-default parameters do not have a ParameterValue, so we'll always override those. - if converted_value != lookup[param_key].get('ParameterValue'): - if lookup[param_key]['IsModifiable']: - modify_list.append(dict(ParameterValue=converted_value, ParameterName=param_key, ApplyMethod=apply_method)) - else: - errors.append("Parameter %s is not modifiable" % param_key) - - # modify_db_parameters takes at most 20 parameters - if modify_list: - try: - from itertools import izip_longest as zip_longest # python 2 - except ImportError: - from itertools import zip_longest # python 3 - for modify_slice in zip_longest(*[iter(modify_list)] * 20, fillvalue=None): - non_empty_slice = [item for item in modify_slice if item] - try: - connection.modify_db_parameter_group(DBParameterGroupName=groupname, Parameters=non_empty_slice) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't update parameters: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - return True, errors - return False, errors - - -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_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 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_absent(module, connection): - group = module.params['name'] - try: - response = connection.describe_db_parameter_groups(DBParameterGroupName=group) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'DBParameterGroupNotFound': - module.exit_json(changed=False) - 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)) - try: - response = connection.delete_db_parameter_group(DBParameterGroupName=group) - module.exit_json(changed=True) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Couldn't delete parameter group: %s" % str(e), - exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(required=True), - engine=dict(), - description=dict(), - params=dict(aliases=['parameters'], type='dict'), - immediate=dict(type='bool', aliases=['apply_immediately']), - tags=dict(type='dict', default={}), - purge_tags=dict(type='bool', default=False) - ) - ) - module = AnsibleModule(argument_spec=argument_spec, - required_if=[['state', 'present', ['description', 'engine']]]) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required for this module') - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - - if not region: - module.fail_json(msg="Region must be present") - - try: - conn = boto3_conn(module, conn_type='client', resource='rds', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Couldn't connect to AWS: %s" % str(e)) - - state = module.params.get('state') - if state == 'present': - ensure_present(module, conn) - if state == 'absent': - ensure_absent(module, conn) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_snapshot.py b/lib/ansible/modules/cloud/amazon/rds_snapshot.py deleted file mode 100644 index 5df2808fee..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_snapshot.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2014 Ansible Project -# Copyright (c) 2017, 2018, 2019 Will Thames -# Copyright (c) 2017, 2018 Michael De La Rue -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: rds_snapshot -version_added: "2.9" -short_description: manage Amazon RDS snapshots. -description: - - Creates or deletes RDS snapshots. -options: - state: - description: - - Specify the desired state of the snapshot. - default: present - choices: [ 'present', 'absent'] - type: str - db_snapshot_identifier: - description: - - The snapshot to manage. - required: true - aliases: - - id - - snapshot_id - type: str - db_instance_identifier: - description: - - Database instance identifier. Required when state is present. - aliases: - - instance_id - type: str - wait: - description: - - Whether or not to wait for snapshot creation or deletion. - type: bool - default: 'no' - wait_timeout: - description: - - how long before wait gives up, in seconds. - default: 300 - type: int - tags: - description: - - tags dict to apply to a snapshot. - type: dict - purge_tags: - description: - - whether to remove tags not present in the C(tags) parameter. - default: True - type: bool -requirements: - - "python >= 2.6" - - "boto3" -author: - - "Will Thames (@willthames)" - - "Michael De La Rue (@mikedlr)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create snapshot -- rds_snapshot: - db_instance_identifier: new-database - db_snapshot_identifier: new-database-snapshot - -# Delete snapshot -- rds_snapshot: - db_snapshot_identifier: new-database-snapshot - state: absent -''' - -RETURN = ''' -allocated_storage: - description: How much storage is allocated in GB. - returned: always - type: int - sample: 20 -availability_zone: - description: Availability zone of the database from which the snapshot was created. - returned: always - type: str - sample: us-west-2a -db_instance_identifier: - description: Database from which the snapshot was created. - returned: always - type: str - sample: ansible-test-16638696 -db_snapshot_arn: - description: Amazon Resource Name for the snapshot. - returned: always - type: str - sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot -db_snapshot_identifier: - description: Name of the snapshot. - returned: always - type: str - sample: ansible-test-16638696-test-snapshot -dbi_resource_id: - description: The identifier for the source DB instance, which can't be changed and which is unique to an AWS Region. - returned: always - type: str - sample: db-MM4P2U35RQRAMWD3QDOXWPZP4U -encrypted: - description: Whether the snapshot is encrypted. - returned: always - type: bool - sample: false -engine: - description: Engine of the database from which the snapshot was created. - returned: always - type: str - sample: mariadb -engine_version: - description: Version of the database from which the snapshot was created. - returned: always - type: str - sample: 10.2.21 -iam_database_authentication_enabled: - description: Whether IAM database authentication is enabled. - returned: always - type: bool - sample: false -instance_create_time: - description: Creation time of the instance from which the snapshot was created. - returned: always - type: str - sample: '2019-06-15T10:15:56.221000+00:00' -license_model: - description: License model of the database. - returned: always - type: str - sample: general-public-license -master_username: - description: Master username of the database. - returned: always - type: str - sample: test -option_group_name: - description: Option group of the database. - returned: always - type: str - sample: default:mariadb-10-2 -percent_progress: - description: How much progress has been made taking the snapshot. Will be 100 for an available snapshot. - returned: always - type: int - sample: 100 -port: - description: Port on which the database is listening. - returned: always - type: int - sample: 3306 -processor_features: - description: List of processor features of the database. - returned: always - type: list - sample: [] -snapshot_create_time: - description: Creation time of the snapshot. - returned: always - type: str - sample: '2019-06-15T10:46:23.776000+00:00' -snapshot_type: - description: How the snapshot was created (always manual for this module!). - returned: always - type: str - sample: manual -status: - description: Status of the snapshot. - returned: always - type: str - sample: available -storage_type: - description: Storage type of the database. - returned: always - type: str - sample: gp2 -tags: - description: Tags applied to the snapshot. - returned: always - type: complex - contains: {} -vpc_id: - description: ID of the VPC in which the DB lives. - returned: always - type: str - sample: vpc-09ff232e222710ae0 -''' - -try: - import botocore -except ImportError: - pass # protected by AnsibleAWSModule - -# import module snippets -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, compare_aws_tags -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list - - -def get_snapshot(client, module, snapshot_id): - try: - response = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id) - except client.exceptions.DBSnapshotNotFoundFault: - return None - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't get snapshot {0}".format(snapshot_id)) - return response['DBSnapshots'][0] - - -def snapshot_to_facts(client, module, snapshot): - try: - snapshot['Tags'] = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'], - aws_retry=True)['TagList']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Couldn't get tags for snapshot %s" % snapshot['DBSnapshotIdentifier']) - except KeyError: - module.fail_json(msg=str(snapshot)) - - return camel_dict_to_snake_dict(snapshot, ignore_list=['Tags']) - - -def wait_for_snapshot_status(client, module, db_snapshot_id, waiter_name): - if not module.params['wait']: - return - timeout = module.params['wait_timeout'] - try: - client.get_waiter(waiter_name).wait(DBSnapshotIdentifier=db_snapshot_id, - WaiterConfig=dict( - Delay=5, - MaxAttempts=int((timeout + 2.5) / 5) - )) - except botocore.exceptions.WaiterError as e: - if waiter_name == 'db_snapshot_deleted': - msg = "Failed to wait for DB snapshot {0} to be deleted".format(db_snapshot_id) - else: - msg = "Failed to wait for DB snapshot {0} to be available".format(db_snapshot_id) - module.fail_json_aws(e, msg=msg) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Failed with an unexpected error while waiting for the DB cluster {0}".format(db_snapshot_id)) - - -def ensure_snapshot_absent(client, module): - snapshot_name = module.params.get('db_snapshot_identifier') - changed = False - - snapshot = get_snapshot(client, module, snapshot_name) - if snapshot and snapshot['Status'] != 'deleting': - try: - client.delete_db_snapshot(DBSnapshotIdentifier=snapshot_name) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="trying to delete snapshot") - - # If we're not waiting for a delete to complete then we're all done - # so just return - if not snapshot or not module.params.get('wait'): - return dict(changed=changed) - try: - wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_deleted') - return dict(changed=changed) - except client.exceptions.DBSnapshotNotFoundFault: - return dict(changed=changed) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "awaiting snapshot deletion") - - -def ensure_tags(client, module, resource_arn, existing_tags, tags, purge_tags): - if tags is None: - return False - tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, tags, purge_tags) - changed = bool(tags_to_add or tags_to_remove) - if tags_to_add: - try: - client.add_tags_to_resource(ResourceName=resource_arn, Tags=ansible_dict_to_boto3_tag_list(tags_to_add)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Couldn't add tags to snapshot {0}".format(resource_arn)) - if tags_to_remove: - try: - client.remove_tags_from_resource(ResourceName=resource_arn, TagKeys=tags_to_remove) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Couldn't remove tags from snapshot {0}".format(resource_arn)) - return changed - - -def ensure_snapshot_present(client, module): - db_instance_identifier = module.params.get('db_instance_identifier') - snapshot_name = module.params.get('db_snapshot_identifier') - changed = False - snapshot = get_snapshot(client, module, snapshot_name) - if not snapshot: - try: - snapshot = client.create_db_snapshot(DBSnapshotIdentifier=snapshot_name, - DBInstanceIdentifier=db_instance_identifier)['DBSnapshot'] - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="trying to create db snapshot") - - if module.params.get('wait'): - wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_available') - - existing_tags = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'], - aws_retry=True)['TagList']) - desired_tags = module.params['tags'] - purge_tags = module.params['purge_tags'] - changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], existing_tags, desired_tags, purge_tags) - - snapshot = get_snapshot(client, module, snapshot_name) - - return dict(changed=changed, **snapshot_to_facts(client, module, snapshot)) - - -def main(): - - module = AnsibleAWSModule( - argument_spec=dict( - state=dict(choices=['present', 'absent'], default='present'), - db_snapshot_identifier=dict(aliases=['id', 'snapshot_id'], required=True), - db_instance_identifier=dict(aliases=['instance_id']), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=300), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=True), - ), - required_if=[['state', 'present', ['db_instance_identifier']]] - ) - - client = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=['DBSnapshotNotFound'])) - - if module.params['state'] == 'absent': - ret_dict = ensure_snapshot_absent(client, module) - else: - ret_dict = ensure_snapshot_present(client, module) - - module.exit_json(**ret_dict) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_snapshot_info.py b/lib/ansible/modules/cloud/amazon/rds_snapshot_info.py deleted file mode 100644 index 96ea044850..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_snapshot_info.py +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2014-2017 Ansible Project -# Copyright (c) 2017, 2018 Will Thames -# Copyright (c) 2017, 2018 Michael De La Rue -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: rds_snapshot_info -version_added: "2.6" -short_description: obtain information about one or more RDS snapshots -description: - - Obtain information about one or more RDS snapshots. These can be for unclustered snapshots or snapshots of clustered DBs (Aurora). - - Aurora snapshot information may be obtained if no identifier parameters are passed or if one of the cluster parameters are passed. - - This module was called C(rds_snapshot_facts) before Ansible 2.9. The usage did not change. -options: - db_snapshot_identifier: - description: - - Name of an RDS (unclustered) snapshot. - - Mutually exclusive with I(db_instance_identifier), I(db_cluster_identifier), I(db_cluster_snapshot_identifier) - required: false - aliases: - - snapshot_name - type: str - db_instance_identifier: - description: - - RDS instance name for which to find snapshots. - - Mutually exclusive with I(db_snapshot_identifier), I(db_cluster_identifier), I(db_cluster_snapshot_identifier) - required: false - type: str - db_cluster_identifier: - description: - - RDS cluster name for which to find snapshots. - - Mutually exclusive with I(db_snapshot_identifier), I(db_instance_identifier), I(db_cluster_snapshot_identifier) - required: false - type: str - db_cluster_snapshot_identifier: - description: - - Name of an RDS cluster snapshot. - - Mutually exclusive with I(db_instance_identifier), I(db_snapshot_identifier), I(db_cluster_identifier) - required: false - type: str - snapshot_type: - description: - - Type of snapshot to find. - - By default both automated and manual snapshots will be returned. - required: false - choices: ['automated', 'manual', 'shared', 'public'] - type: str -requirements: - - "python >= 2.6" - - "boto3" -author: - - "Will Thames (@willthames)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Get information about an snapshot -- rds_snapshot_info: - db_snapshot_identifier: snapshot_name - register: new_database_info - -# Get all RDS snapshots for an RDS instance -- rds_snapshot_info: - db_instance_identifier: helloworld-rds-master -''' - -RETURN = ''' -snapshots: - description: List of non-clustered snapshots - returned: When cluster parameters are not passed - type: complex - contains: - allocated_storage: - description: How many gigabytes of storage are allocated - returned: always - type: int - sample: 10 - availability_zone: - description: The availability zone of the database from which the snapshot was taken - returned: always - type: str - sample: us-west-2b - db_instance_identifier: - description: Database instance identifier - returned: always - type: str - sample: hello-world-rds - db_snapshot_arn: - description: Snapshot ARN - returned: always - type: str - sample: arn:aws:rds:us-west-2:111111111111:snapshot:rds:hello-world-rds-us1-2018-05-16-04-03 - db_snapshot_identifier: - description: Snapshot name - returned: always - type: str - sample: rds:hello-world-rds-us1-2018-05-16-04-03 - encrypted: - description: Whether the snapshot was encrypted - returned: always - type: bool - sample: true - engine: - description: Database engine - returned: always - type: str - sample: postgres - engine_version: - description: Database engine version - returned: always - type: str - sample: 9.5.10 - iam_database_authentication_enabled: - description: Whether database authentication through IAM is enabled - returned: always - type: bool - sample: false - instance_create_time: - description: Time the Instance was created - returned: always - type: str - sample: '2017-10-10T04:00:07.434000+00:00' - kms_key_id: - description: ID of the KMS Key encrypting the snapshot - returned: always - type: str - sample: arn:aws:kms:us-west-2:111111111111:key/abcd1234-1234-aaaa-0000-1234567890ab - license_model: - description: License model - returned: always - type: str - sample: postgresql-license - master_username: - description: Database master username - returned: always - type: str - sample: dbadmin - option_group_name: - description: Database option group name - returned: always - type: str - sample: default:postgres-9-5 - percent_progress: - description: Percent progress of snapshot - returned: always - type: int - sample: 100 - snapshot_create_time: - description: Time snapshot was created - returned: always - type: str - sample: '2018-05-16T04:03:33.871000+00:00' - snapshot_type: - description: Type of snapshot - returned: always - type: str - sample: automated - status: - description: Status of snapshot - returned: always - type: str - sample: available - storage_type: - description: Storage type of underlying DB - returned: always - type: str - sample: gp2 - tags: - description: Snapshot tags - returned: when snapshot is not shared - type: complex - contains: {} - vpc_id: - description: ID of VPC containing the DB - returned: always - type: str - sample: vpc-abcd1234 -cluster_snapshots: - description: List of cluster snapshots - returned: always - type: complex - contains: - allocated_storage: - description: How many gigabytes of storage are allocated - returned: always - type: int - sample: 1 - availability_zones: - description: The availability zones of the database from which the snapshot was taken - returned: always - type: list - sample: - - ca-central-1a - - ca-central-1b - cluster_create_time: - description: Date and time the cluster was created - returned: always - type: str - sample: '2018-05-17T00:13:40.223000+00:00' - db_cluster_identifier: - description: Database cluster identifier - returned: always - type: str - sample: test-aurora-cluster - db_cluster_snapshot_arn: - description: ARN of the database snapshot - returned: always - type: str - sample: arn:aws:rds:ca-central-1:111111111111:cluster-snapshot:test-aurora-snapshot - db_cluster_snapshot_identifier: - description: Snapshot identifier - returned: always - type: str - sample: test-aurora-snapshot - engine: - description: Database engine - returned: always - type: str - sample: aurora - engine_version: - description: Database engine version - returned: always - type: str - sample: 5.6.10a - iam_database_authentication_enabled: - description: Whether database authentication through IAM is enabled - returned: always - type: bool - sample: false - kms_key_id: - description: ID of the KMS Key encrypting the snapshot - returned: always - type: str - sample: arn:aws:kms:ca-central-1:111111111111:key/abcd1234-abcd-1111-aaaa-0123456789ab - license_model: - description: License model - returned: always - type: str - sample: aurora - master_username: - description: Database master username - returned: always - type: str - sample: shertel - percent_progress: - description: Percent progress of snapshot - returned: always - type: int - sample: 0 - port: - description: Database port - returned: always - type: int - sample: 0 - snapshot_create_time: - description: Date and time when the snapshot was created - returned: always - type: str - sample: '2018-05-17T00:23:23.731000+00:00' - snapshot_type: - description: Type of snapshot - returned: always - type: str - sample: manual - status: - description: Status of snapshot - returned: always - type: str - sample: creating - storage_encrypted: - description: Whether the snapshot is encrypted - returned: always - type: bool - sample: true - tags: - description: Tags of the snapshot - returned: when snapshot is not shared - type: complex - contains: {} - vpc_id: - description: VPC of the database - returned: always - type: str - sample: vpc-abcd1234 -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import AWSRetry, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict - -try: - import botocore -except Exception: - pass # caught by AnsibleAWSModule - - -def common_snapshot_info(module, conn, method, prefix, params): - paginator = conn.get_paginator(method) - try: - results = paginator.paginate(**params).build_full_result()['%ss' % prefix] - except is_boto3_error_code('%sNotFound' % prefix): - results = [] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, "trying to get snapshot information") - - for snapshot in results: - try: - if snapshot['SnapshotType'] != 'shared': - snapshot['Tags'] = boto3_tag_list_to_ansible_dict(conn.list_tags_for_resource(ResourceName=snapshot['%sArn' % prefix], - aws_retry=True)['TagList']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Couldn't get tags for snapshot %s" % snapshot['%sIdentifier' % prefix]) - - return [camel_dict_to_snake_dict(snapshot, ignore_list=['Tags']) for snapshot in results] - - -def cluster_snapshot_info(module, conn): - snapshot_name = module.params.get('db_cluster_snapshot_identifier') - snapshot_type = module.params.get('snapshot_type') - instance_name = module.params.get('db_cluster_identifier') - - params = dict() - if snapshot_name: - params['DBClusterSnapshotIdentifier'] = snapshot_name - if instance_name: - params['DBClusterIdentifier'] = instance_name - if snapshot_type: - params['SnapshotType'] = snapshot_type - if snapshot_type == 'public': - params['IncludePublic'] = True - elif snapshot_type == 'shared': - params['IncludeShared'] = True - - return common_snapshot_info(module, conn, 'describe_db_cluster_snapshots', 'DBClusterSnapshot', params) - - -def standalone_snapshot_info(module, conn): - snapshot_name = module.params.get('db_snapshot_identifier') - snapshot_type = module.params.get('snapshot_type') - instance_name = module.params.get('db_instance_identifier') - - params = dict() - if snapshot_name: - params['DBSnapshotIdentifier'] = snapshot_name - if instance_name: - params['DBInstanceIdentifier'] = instance_name - if snapshot_type: - params['SnapshotType'] = snapshot_type - if snapshot_type == 'public': - params['IncludePublic'] = True - elif snapshot_type == 'shared': - params['IncludeShared'] = True - - return common_snapshot_info(module, conn, 'describe_db_snapshots', 'DBSnapshot', params) - - -def main(): - argument_spec = dict( - db_snapshot_identifier=dict(aliases=['snapshot_name']), - db_instance_identifier=dict(), - db_cluster_identifier=dict(), - db_cluster_snapshot_identifier=dict(), - snapshot_type=dict(choices=['automated', 'manual', 'shared', 'public']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['db_snapshot_identifier', 'db_instance_identifier', 'db_cluster_identifier', 'db_cluster_snapshot_identifier']] - ) - if module._name == 'rds_snapshot_facts': - module.deprecate("The 'rds_snapshot_facts' module has been renamed to 'rds_snapshot_info'", version='2.13') - - conn = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10)) - results = dict() - if not module.params['db_cluster_identifier'] and not module.params['db_cluster_snapshot_identifier']: - results['snapshots'] = standalone_snapshot_info(module, conn) - if not module.params['db_snapshot_identifier'] and not module.params['db_instance_identifier']: - results['cluster_snapshots'] = cluster_snapshot_info(module, conn) - - module.exit_json(changed=False, **results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/rds_subnet_group.py b/lib/ansible/modules/cloud/amazon/rds_subnet_group.py deleted file mode 100644 index 008a7dc259..0000000000 --- a/lib/ansible/modules/cloud/amazon/rds_subnet_group.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: rds_subnet_group -version_added: "1.5" -short_description: manage RDS database subnet groups -description: - - Creates, modifies, and deletes RDS database subnet groups. This module has a dependency on python-boto >= 2.5. -options: - state: - description: - - Specifies whether the subnet should be present or absent. - required: true - choices: [ 'present' , 'absent' ] - type: str - name: - description: - - Database subnet group identifier. - required: true - type: str - description: - description: - - Database subnet group description. - - Required when I(state=present). - type: str - subnets: - description: - - List of subnet IDs that make up the database subnet group. - - Required when I(state=present). - type: list -author: "Scott Anderson (@tastychutney)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Add or change a subnet group -- rds_subnet_group: - state: present - name: norwegian-blue - description: My Fancy Ex Parrot Subnet Group - subnets: - - subnet-aaaaaaaa - - subnet-bbbbbbbb - -# Remove a subnet group -- rds_subnet_group: - state: absent - name: norwegian-blue -''' - -RETURN = ''' -subnet_group: - description: Dictionary of DB subnet group values - returned: I(state=present) - type: complex - contains: - name: - description: The name of the DB subnet group - returned: I(state=present) - type: str - description: - description: The description of the DB subnet group - returned: I(state=present) - type: str - vpc_id: - description: The VpcId of the DB subnet group - returned: I(state=present) - type: str - subnet_ids: - description: Contains a list of Subnet IDs - returned: I(state=present) - type: list - status: - description: The status of the DB subnet group - returned: I(state=present) - type: str -''' - -try: - import boto.rds - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info - - -def get_subnet_group_info(subnet_group): - return dict( - name=subnet_group.name, - description=subnet_group.description, - vpc_id=subnet_group.vpc_id, - subnet_ids=subnet_group.subnet_ids, - status=subnet_group.status - ) - - -def create_result(changed, subnet_group=None): - if subnet_group is None: - return dict( - changed=changed - ) - else: - return dict( - changed=changed, - subnet_group=get_subnet_group_info(subnet_group) - ) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(required=True), - description=dict(required=False), - subnets=dict(required=False, type='list'), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - state = module.params.get('state') - group_name = module.params.get('name').lower() - group_description = module.params.get('description') - group_subnets = module.params.get('subnets') or {} - - if state == 'present': - for required in ['description', 'subnets']: - if not module.params.get(required): - module.fail_json(msg=str("Parameter %s required for state='present'" % required)) - else: - for not_allowed in ['description', 'subnets']: - if module.params.get(not_allowed): - module.fail_json(msg=str("Parameter %s not allowed for state='absent'" % not_allowed)) - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - if not region: - module.fail_json(msg=str("Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.")) - - try: - conn = connect_to_aws(boto.rds, region, **aws_connect_kwargs) - except BotoServerError as e: - module.fail_json(msg=e.error_message) - - try: - exists = False - result = create_result(False) - - try: - matching_groups = conn.get_all_db_subnet_groups(group_name, max_records=100) - exists = len(matching_groups) > 0 - except BotoServerError as e: - if e.error_code != 'DBSubnetGroupNotFoundFault': - module.fail_json(msg=e.error_message) - - if state == 'absent': - if exists: - conn.delete_db_subnet_group(group_name) - result = create_result(True) - else: - if not exists: - new_group = conn.create_db_subnet_group(group_name, desc=group_description, subnet_ids=group_subnets) - result = create_result(True, new_group) - else: - # Sort the subnet groups before we compare them - matching_groups[0].subnet_ids.sort() - group_subnets.sort() - if (matching_groups[0].name != group_name or - matching_groups[0].description != group_description or - matching_groups[0].subnet_ids != group_subnets): - changed_group = conn.modify_db_subnet_group(group_name, description=group_description, subnet_ids=group_subnets) - result = create_result(True, changed_group) - else: - result = create_result(False, matching_groups[0]) - except BotoServerError as e: - module.fail_json(msg=e.error_message) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/redshift.py b/lib/ansible/modules/cloud/amazon/redshift.py deleted file mode 100644 index 48d1d46abc..0000000000 --- a/lib/ansible/modules/cloud/amazon/redshift.py +++ /dev/null @@ -1,625 +0,0 @@ -#!/usr/bin/python - -# Copyright 2014 Jens Carl, Hothead Games Inc. -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -author: - - "Jens Carl (@j-carl), Hothead Games Inc." - - "Rafael Driutti (@rafaeldriutti)" -module: redshift -version_added: "2.2" -short_description: create, delete, or modify an Amazon Redshift instance -description: - - Creates, deletes, or modifies Amazon Redshift cluster instances. -options: - command: - description: - - Specifies the action to take. - required: true - choices: [ 'create', 'facts', 'delete', 'modify' ] - type: str - identifier: - description: - - Redshift cluster identifier. - required: true - type: str - node_type: - description: - - The node type of the cluster. - - Require when I(command=create). - choices: ['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', 'ds2.8xlarge', 'dc1.large','dc2.large', - 'dc1.8xlarge', 'dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge'] - type: str - username: - description: - - Master database username. - - Used only when I(command=create). - type: str - password: - description: - - Master database password. - - Used only when I(command=create). - type: str - cluster_type: - description: - - The type of cluster. - choices: ['multi-node', 'single-node' ] - default: 'single-node' - type: str - db_name: - description: - - Name of the database. - type: str - availability_zone: - description: - - Availability zone in which to launch cluster. - aliases: ['zone', 'aws_zone'] - type: str - number_of_nodes: - description: - - Number of nodes. - - Only used when I(cluster_type=multi-node). - type: int - cluster_subnet_group_name: - description: - - Which subnet to place the cluster. - aliases: ['subnet'] - type: str - cluster_security_groups: - description: - - In which security group the cluster belongs. - type: list - elements: str - aliases: ['security_groups'] - vpc_security_group_ids: - description: - - VPC security group - aliases: ['vpc_security_groups'] - type: list - elements: str - skip_final_cluster_snapshot: - description: - - Skip a final snapshot before deleting the cluster. - - Used only when I(command=delete). - aliases: ['skip_final_snapshot'] - default: false - version_added: "2.4" - type: bool - final_cluster_snapshot_identifier: - description: - - Identifier of the final snapshot to be created before deleting the cluster. - - If this parameter is provided, I(skip_final_cluster_snapshot) must be C(false). - - Used only when I(command=delete). - aliases: ['final_snapshot_id'] - version_added: "2.4" - type: str - preferred_maintenance_window: - description: - - 'Maintenance window in format of C(ddd:hh24:mi-ddd:hh24:mi). (Example: C(Mon:22:00-Mon:23:15))' - - Times are specified in UTC. - - If not specified then a random 30 minute maintenance window is assigned. - aliases: ['maintance_window', 'maint_window'] - type: str - cluster_parameter_group_name: - description: - - Name of the cluster parameter group. - aliases: ['param_group_name'] - type: str - automated_snapshot_retention_period: - description: - - The number of days that automated snapshots are retained. - aliases: ['retention_period'] - type: int - port: - description: - - Which port the cluster is listening on. - type: int - cluster_version: - description: - - Which version the cluster should have. - aliases: ['version'] - choices: ['1.0'] - type: str - allow_version_upgrade: - description: - - When I(allow_version_upgrade=true) the cluster may be automatically - upgraded during the maintenance window. - aliases: ['version_upgrade'] - default: true - type: bool - publicly_accessible: - description: - - If the cluster is accessible publicly or not. - default: false - type: bool - encrypted: - description: - - If the cluster is encrypted or not. - default: false - type: bool - elastic_ip: - description: - - An Elastic IP to use for the cluster. - type: str - new_cluster_identifier: - description: - - Only used when command=modify. - aliases: ['new_identifier'] - type: str - wait: - description: - - When I(command=create), I(command=modify) or I(command=restore) then wait for the database to enter the 'available' state. - - When I(command=delete) wait for the database to be terminated. - type: bool - default: false - wait_timeout: - description: - - When I(wait=true) defines how long in seconds before giving up. - default: 300 - type: int - enhanced_vpc_routing: - description: - - Whether the cluster should have enhanced VPC routing enabled. - default: false - type: bool -requirements: [ 'boto3' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Basic cluster provisioning example -- redshift: > - command=create - node_type=ds1.xlarge - identifier=new_cluster - username=cluster_admin - password=1nsecure - -# Cluster delete example -- redshift: - command: delete - identifier: new_cluster - skip_final_cluster_snapshot: true - wait: true -''' - -RETURN = ''' -cluster: - description: dictionary containing all the cluster information - returned: success - type: complex - contains: - identifier: - description: Id of the cluster. - returned: success - type: str - sample: "new_redshift_cluster" - create_time: - description: Time of the cluster creation as timestamp. - returned: success - type: float - sample: 1430158536.308 - status: - description: Status of the cluster. - returned: success - type: str - sample: "available" - db_name: - description: Name of the database. - returned: success - type: str - sample: "new_db_name" - availability_zone: - description: Amazon availability zone where the cluster is located. "None" until cluster is available. - returned: success - type: str - sample: "us-east-1b" - maintenance_window: - description: Time frame when maintenance/upgrade are done. - returned: success - type: str - sample: "sun:09:30-sun:10:00" - private_ip_address: - description: Private IP address of the main node. - returned: success - type: str - sample: "10.10.10.10" - public_ip_address: - description: Public IP address of the main node. "None" when enhanced_vpc_routing is enabled. - returned: success - type: str - sample: "0.0.0.0" - port: - description: Port of the cluster. "None" until cluster is available. - returned: success - type: int - sample: 5439 - url: - description: FQDN of the main cluster node. "None" until cluster is available. - returned: success - type: str - sample: "new-redshift_cluster.jfkdjfdkj.us-east-1.redshift.amazonaws.com" - enhanced_vpc_routing: - description: status of the enhanced vpc routing feature. - returned: success - type: bool -''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.ec2 import AWSRetry, snake_dict_to_camel_dict -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code - - -def _collect_facts(resource): - """Transform cluster information to dict.""" - facts = { - 'identifier': resource['ClusterIdentifier'], - 'status': resource['ClusterStatus'], - 'username': resource['MasterUsername'], - 'db_name': resource['DBName'], - 'maintenance_window': resource['PreferredMaintenanceWindow'], - 'enhanced_vpc_routing': resource['EnhancedVpcRouting'] - - } - - for node in resource['ClusterNodes']: - if node['NodeRole'] in ('SHARED', 'LEADER'): - facts['private_ip_address'] = node['PrivateIPAddress'] - if facts['enhanced_vpc_routing'] is False: - facts['public_ip_address'] = node['PublicIPAddress'] - else: - facts['public_ip_address'] = None - break - - # Some parameters are not ready instantly if you don't wait for available - # cluster status - facts['create_time'] = None - facts['url'] = None - facts['port'] = None - facts['availability_zone'] = None - - if resource['ClusterStatus'] != "creating": - facts['create_time'] = resource['ClusterCreateTime'] - facts['url'] = resource['Endpoint']['Address'] - facts['port'] = resource['Endpoint']['Port'] - facts['availability_zone'] = resource['AvailabilityZone'] - - return facts - - -@AWSRetry.jittered_backoff() -def _describe_cluster(redshift, identifier): - ''' - Basic wrapper around describe_clusters with a retry applied - ''' - return redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0] - - -@AWSRetry.jittered_backoff() -def _create_cluster(redshift, **kwargs): - ''' - Basic wrapper around create_cluster with a retry applied - ''' - return redshift.create_cluster(**kwargs) - - -# Simple wrapper around delete, try to avoid throwing an error if some other -# operation is in progress -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidClusterState']) -def _delete_cluster(redshift, **kwargs): - ''' - Basic wrapper around delete_cluster with a retry applied. - Explicitly catches 'InvalidClusterState' (~ Operation in progress) so that - we can still delete a cluster if some kind of change operation was in - progress. - ''' - return redshift.delete_cluster(**kwargs) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidClusterState']) -def _modify_cluster(redshift, **kwargs): - ''' - Basic wrapper around modify_cluster with a retry applied. - Explicitly catches 'InvalidClusterState' (~ Operation in progress) for cases - where another modification is still in progress - ''' - return redshift.modify_cluster(**kwargs) - - -def create_cluster(module, redshift): - """ - Create a new cluster - - module: AnsibleModule object - redshift: authenticated redshift connection object - - Returns: - """ - - identifier = module.params.get('identifier') - node_type = module.params.get('node_type') - username = module.params.get('username') - password = module.params.get('password') - d_b_name = module.params.get('db_name') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - changed = True - # Package up the optional parameters - params = {} - for p in ('cluster_type', 'cluster_security_groups', - 'vpc_security_group_ids', 'cluster_subnet_group_name', - 'availability_zone', 'preferred_maintenance_window', - 'cluster_parameter_group_name', - 'automated_snapshot_retention_period', 'port', - 'cluster_version', 'allow_version_upgrade', - 'number_of_nodes', 'publicly_accessible', - 'encrypted', 'elastic_ip', 'enhanced_vpc_routing'): - # https://github.com/boto/boto3/issues/400 - if module.params.get(p) is not None: - params[p] = module.params.get(p) - - if d_b_name: - params['d_b_name'] = d_b_name - - try: - _describe_cluster(redshift, identifier) - changed = False - except is_boto3_error_code('ClusterNotFound'): - try: - _create_cluster(redshift, - ClusterIdentifier=identifier, - NodeType=node_type, - MasterUsername=username, - MasterUserPassword=password, - **snake_dict_to_camel_dict(params, capitalize_first=True)) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Failed to create cluster") - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Failed to describe cluster") - if wait: - attempts = wait_timeout // 60 - waiter = redshift.get_waiter('cluster_available') - try: - waiter.wait( - ClusterIdentifier=identifier, - WaiterConfig=dict(MaxAttempts=attempts) - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Timeout waiting for the cluster creation") - try: - resource = _describe_cluster(redshift, identifier) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Failed to describe cluster") - - return(changed, _collect_facts(resource)) - - -def describe_cluster(module, redshift): - """ - Collect data about the cluster. - - module: Ansible module object - redshift: authenticated redshift connection object - """ - identifier = module.params.get('identifier') - - try: - resource = _describe_cluster(redshift, identifier) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error describing cluster") - - return(True, _collect_facts(resource)) - - -def delete_cluster(module, redshift): - """ - Delete a cluster. - - module: Ansible module object - redshift: authenticated redshift connection object - """ - - identifier = module.params.get('identifier') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - params = {} - for p in ('skip_final_cluster_snapshot', - 'final_cluster_snapshot_identifier'): - if p in module.params: - # https://github.com/boto/boto3/issues/400 - if module.params.get(p) is not None: - params[p] = module.params.get(p) - - try: - _delete_cluster( - redshift, - ClusterIdentifier=identifier, - **snake_dict_to_camel_dict(params, capitalize_first=True)) - except is_boto3_error_code('ClusterNotFound'): - return(False, {}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Failed to delete cluster") - - if wait: - attempts = wait_timeout // 60 - waiter = redshift.get_waiter('cluster_deleted') - try: - waiter.wait( - ClusterIdentifier=identifier, - WaiterConfig=dict(MaxAttempts=attempts) - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Timeout deleting the cluster") - - return(True, {}) - - -def modify_cluster(module, redshift): - """ - Modify an existing cluster. - - module: Ansible module object - redshift: authenticated redshift connection object - """ - - identifier = module.params.get('identifier') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - # Package up the optional parameters - params = {} - for p in ('cluster_type', 'cluster_security_groups', - 'vpc_security_group_ids', 'cluster_subnet_group_name', - 'availability_zone', 'preferred_maintenance_window', - 'cluster_parameter_group_name', - 'automated_snapshot_retention_period', 'port', 'cluster_version', - 'allow_version_upgrade', 'number_of_nodes', 'new_cluster_identifier'): - # https://github.com/boto/boto3/issues/400 - if module.params.get(p) is not None: - params[p] = module.params.get(p) - - # enhanced_vpc_routing parameter change needs an exclusive request - if module.params.get('enhanced_vpc_routing') is not None: - try: - _modify_cluster( - redshift, - ClusterIdentifier=identifier, - EnhancedVpcRouting=module.params.get('enhanced_vpc_routing')) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier) - if wait: - attempts = wait_timeout // 60 - waiter = redshift.get_waiter('cluster_available') - try: - waiter.wait( - ClusterIdentifier=identifier, - WaiterConfig=dict(MaxAttempts=attempts) - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, - msg="Timeout waiting for cluster enhanced vpc routing modification" - ) - - # change the rest - try: - _modify_cluster( - redshift, - ClusterIdentifier=identifier, - **snake_dict_to_camel_dict(params, capitalize_first=True)) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier) - - if module.params.get('new_cluster_identifier'): - identifier = module.params.get('new_cluster_identifier') - - if wait: - attempts = wait_timeout // 60 - waiter2 = redshift.get_waiter('cluster_available') - try: - waiter2.wait( - ClusterIdentifier=identifier, - WaiterConfig=dict(MaxAttempts=attempts) - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Timeout waiting for cluster modification") - try: - resource = _describe_cluster(redshift, identifier) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json(e, msg="Couldn't modify redshift cluster %s " % identifier) - - return(True, _collect_facts(resource)) - - -def main(): - argument_spec = dict( - command=dict(choices=['create', 'facts', 'delete', 'modify'], required=True), - identifier=dict(required=True), - node_type=dict(choices=['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', - 'ds2.8xlarge', 'dc1.large', 'dc2.large', - 'dc1.8xlarge', 'dw1.xlarge', 'dw1.8xlarge', - 'dw2.large', 'dw2.8xlarge'], required=False), - username=dict(required=False), - password=dict(no_log=True, required=False), - db_name=dict(required=False), - cluster_type=dict(choices=['multi-node', 'single-node'], default='single-node'), - cluster_security_groups=dict(aliases=['security_groups'], type='list'), - vpc_security_group_ids=dict(aliases=['vpc_security_groups'], type='list'), - skip_final_cluster_snapshot=dict(aliases=['skip_final_snapshot'], - type='bool', default=False), - final_cluster_snapshot_identifier=dict(aliases=['final_snapshot_id'], required=False), - cluster_subnet_group_name=dict(aliases=['subnet']), - availability_zone=dict(aliases=['aws_zone', 'zone']), - preferred_maintenance_window=dict(aliases=['maintance_window', 'maint_window']), - cluster_parameter_group_name=dict(aliases=['param_group_name']), - automated_snapshot_retention_period=dict(aliases=['retention_period'], type='int'), - port=dict(type='int'), - cluster_version=dict(aliases=['version'], choices=['1.0']), - allow_version_upgrade=dict(aliases=['version_upgrade'], type='bool', default=True), - number_of_nodes=dict(type='int'), - publicly_accessible=dict(type='bool', default=False), - encrypted=dict(type='bool', default=False), - elastic_ip=dict(required=False), - new_cluster_identifier=dict(aliases=['new_identifier']), - enhanced_vpc_routing=dict(type='bool', default=False), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=300), - ) - - required_if = [ - ('command', 'delete', ['skip_final_cluster_snapshot']), - ('command', 'create', ['node_type', - 'username', - 'password']) - ] - - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_if=required_if - ) - - command = module.params.get('command') - skip_final_cluster_snapshot = module.params.get('skip_final_cluster_snapshot') - final_cluster_snapshot_identifier = module.params.get('final_cluster_snapshot_identifier') - # can't use module basic required_if check for this case - if command == 'delete' and skip_final_cluster_snapshot is False and final_cluster_snapshot_identifier is None: - module.fail_json(msg="Need to specify final_cluster_snapshot_identifier if skip_final_cluster_snapshot is False") - - conn = module.client('redshift') - - changed = True - if command == 'create': - (changed, cluster) = create_cluster(module, conn) - - elif command == 'facts': - (changed, cluster) = describe_cluster(module, conn) - - elif command == 'delete': - (changed, cluster) = delete_cluster(module, conn) - - elif command == 'modify': - (changed, cluster) = modify_cluster(module, conn) - - module.exit_json(changed=changed, cluster=cluster) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/redshift_cross_region_snapshots.py b/lib/ansible/modules/cloud/amazon/redshift_cross_region_snapshots.py deleted file mode 100644 index 42d93228e6..0000000000 --- a/lib/ansible/modules/cloud/amazon/redshift_cross_region_snapshots.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, JR Kerkstra <jrkerkstra@example.org> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'metadata_version': '1.1'} - -DOCUMENTATION = ''' ---- -module: redshift_cross_region_snapshots -short_description: Manage Redshift Cross Region Snapshots -description: - - Manage Redshift Cross Region Snapshots. Supports KMS-Encrypted Snapshots. - - For more information, see U(https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-snapshots.html#cross-region-snapshot-copy) -version_added: "2.8" -author: JR Kerkstra (@captainkerk) -options: - cluster_name: - description: - - The name of the cluster to configure cross-region snapshots for. - required: true - aliases: [ "cluster" ] - type: str - state: - description: - - Create or remove the cross-region snapshot configuration. - choices: [ "present", "absent" ] - default: present - type: str - region: - description: - - "The cluster's region." - required: true - aliases: [ "source" ] - type: str - destination_region: - description: - - The region to copy snapshots to. - required: true - aliases: [ "destination" ] - type: str - snapshot_copy_grant: - description: - - A grant for Amazon Redshift to use a master key in the I(destination_region). - - See U(http://boto3.readthedocs.io/en/latest/reference/services/redshift.html#Redshift.Client.create_snapshot_copy_grant) - aliases: [ "copy_grant" ] - type: str - snapshot_retention_period: - description: - - The number of days to keep cross-region snapshots for. - required: true - aliases: [ "retention_period" ] - type: int -requirements: [ "botocore", "boto3" ] -extends_documentation_fragment: - - ec2 - - aws -''' - -EXAMPLES = ''' -- name: configure cross-region snapshot on cluster `johniscool` - redshift_cross_region_snapshots: - cluster_name: johniscool - state: present - region: us-east-1 - destination_region: us-west-2 - retention_period: 1 - -- name: configure cross-region snapshot on kms-encrypted cluster - redshift_cross_region_snapshots: - cluster_name: whatever - state: present - region: us-east-1 - destination: us-west-2 - copy_grant: 'my-grant-in-destination' - retention_period: 10 - -- name: disable cross-region snapshots, necessary before most cluster modifications (rename, resize) - redshift_cross_region_snapshots: - cluster_name: whatever - state: absent - region: us-east-1 - destination_region: us-west-2 -''' - -RETURN = ''' # ''' - -from ansible.module_utils.aws.core import AnsibleAWSModule - - -class SnapshotController(object): - - def __init__(self, client, cluster_name): - self.client = client - self.cluster_name = cluster_name - - def get_cluster_snapshot_copy_status(self): - response = self.client.describe_clusters( - ClusterIdentifier=self.cluster_name - ) - return response['Clusters'][0].get('ClusterSnapshotCopyStatus') - - def enable_snapshot_copy(self, destination_region, grant_name, retention_period): - if grant_name: - self.client.enable_snapshot_copy( - ClusterIdentifier=self.cluster_name, - DestinationRegion=destination_region, - RetentionPeriod=retention_period, - SnapshotCopyGrantName=grant_name, - ) - else: - self.client.enable_snapshot_copy( - ClusterIdentifier=self.cluster_name, - DestinationRegion=destination_region, - RetentionPeriod=retention_period, - ) - - def disable_snapshot_copy(self): - self.client.disable_snapshot_copy( - ClusterIdentifier=self.cluster_name - ) - - def modify_snapshot_copy_retention_period(self, retention_period): - self.client.modify_snapshot_copy_retention_period( - ClusterIdentifier=self.cluster_name, - RetentionPeriod=retention_period - ) - - -def requesting_unsupported_modifications(actual, requested): - if (actual['SnapshotCopyGrantName'] != requested['snapshot_copy_grant'] or - actual['DestinationRegion'] != requested['destination_region']): - return True - return False - - -def needs_update(actual, requested): - if actual['RetentionPeriod'] != requested['snapshot_retention_period']: - return True - return False - - -def run_module(): - argument_spec = dict( - cluster_name=dict(type='str', required=True, aliases=['cluster']), - state=dict(type='str', choices=['present', 'absent'], default='present'), - region=dict(type='str', required=True, aliases=['source']), - destination_region=dict(type='str', required=True, aliases=['destination']), - snapshot_copy_grant=dict(type='str', aliases=['copy_grant']), - snapshot_retention_period=dict(type='int', required=True, aliases=['retention_period']), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - result = dict( - changed=False, - message='' - ) - connection = module.client('redshift') - - snapshot_controller = SnapshotController(client=connection, - cluster_name=module.params.get('cluster_name')) - - current_config = snapshot_controller.get_cluster_snapshot_copy_status() - if current_config is not None: - if module.params.get('state') == 'present': - if requesting_unsupported_modifications(current_config, module.params): - message = 'Cannot modify destination_region or grant_name. ' \ - 'Please disable cross-region snapshots, and re-run.' - module.fail_json(msg=message, **result) - if needs_update(current_config, module.params): - result['changed'] = True - if not module.check_mode: - snapshot_controller.modify_snapshot_copy_retention_period( - module.params.get('snapshot_retention_period') - ) - else: - result['changed'] = True - if not module.check_mode: - snapshot_controller.disable_snapshot_copy() - else: - if module.params.get('state') == 'present': - result['changed'] = True - if not module.check_mode: - snapshot_controller.enable_snapshot_copy(module.params.get('destination_region'), - module.params.get('snapshot_copy_grant'), - module.params.get('snapshot_retention_period')) - module.exit_json(**result) - - -def main(): - run_module() - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/redshift_info.py b/lib/ansible/modules/cloud/amazon/redshift_info.py deleted file mode 100644 index 693576a2a5..0000000000 --- a/lib/ansible/modules/cloud/amazon/redshift_info.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: redshift_info -author: "Jens Carl (@j-carl)" -short_description: Gather information about Redshift cluster(s) -description: - - Gather information about Redshift cluster(s). - - This module was called C(redshift_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -requirements: [ boto3 ] -options: - cluster_identifier: - description: - - The prefix of cluster identifier of the Redshift cluster you are searching for. - - "This is a regular expression match with implicit '^'. Append '$' for a complete match." - required: false - aliases: ['name', 'identifier'] - type: str - tags: - description: - - "A dictionary/hash of tags in the format { tag1_name: 'tag1_value', tag2_name: 'tag2_value' } - to match against the security group(s) you are searching for." - required: false - type: dict -extends_documentation_fragment: - - ec2 - - aws -''' - -EXAMPLES = ''' -# Note: These examples do net set authentication details, see the AWS guide for details. - -# Find all clusters -- redshift_info: - register: redshift - -# Find cluster(s) with matching tags -- redshift_info: - tags: - env: prd - stack: monitoring - register: redshift_tags - -# Find cluster(s) with matching name/prefix and tags -- redshift_info: - tags: - env: dev - stack: web - name: user- - register: redshift_web - -# Fail if no cluster(s) is/are found -- redshift_info: - tags: - env: stg - stack: db - register: redshift_user - failed_when: "{{ redshift_user.results | length == 0 }}" -''' - -RETURN = ''' -# For more information see U(http://boto3.readthedocs.io/en/latest/reference/services/redshift.html#Redshift.Client.describe_clusters) ---- -cluster_identifier: - description: Unique key to identify the cluster. - returned: success - type: str - sample: "redshift-identifier" -node_type: - description: The node type for nodes in the cluster. - returned: success - type: str - sample: "ds2.xlarge" -cluster_status: - description: Current state of the cluster. - returned: success - type: str - sample: "available" -modify_status: - description: The status of a modify operation. - returned: optional - type: str - sample: "" -master_username: - description: The master user name for the cluster. - returned: success - type: str - sample: "admin" -db_name: - description: The name of the initial database that was created when the cluster was created. - returned: success - type: str - sample: "dev" -endpoint: - description: The connection endpoint. - returned: success - type: str - sample: { - "address": "cluster-ds2.ocmugla0rf.us-east-1.redshift.amazonaws.com", - "port": 5439 - } -cluster_create_time: - description: The date and time that the cluster was created. - returned: success - type: str - sample: "2016-05-10T08:33:16.629000+00:00" -automated_snapshot_retention_period: - description: The number of days that automatic cluster snapshots are retained. - returned: success - type: int - sample: 1 -cluster_security_groups: - description: A list of cluster security groups that are associated with the cluster. - returned: success - type: list - sample: [] -vpc_security_groups: - description: A list of VPC security groups the are associated with the cluster. - returned: success - type: list - sample: [ - { - "status": "active", - "vpc_security_group_id": "sg-12cghhg" - } - ] -cluster_paramater_groups: - description: The list of cluster parameters that are associated with this cluster. - returned: success - type: list - sample: [ - { - "cluster_parameter_status_list": [ - { - "parameter_apply_status": "in-sync", - "parameter_name": "statement_timeout" - }, - { - "parameter_apply_status": "in-sync", - "parameter_name": "require_ssl" - } - ], - "parameter_apply_status": "in-sync", - "parameter_group_name": "tuba" - } - ] -cluster_subnet_group_name: - description: The name of the subnet group that is associated with the cluster. - returned: success - type: str - sample: "redshift-subnet" -vpc_id: - description: The identifier of the VPC the cluster is in, if the cluster is in a VPC. - returned: success - type: str - sample: "vpc-1234567" -availability_zone: - description: The name of the Availability Zone in which the cluster is located. - returned: success - type: str - sample: "us-east-1b" -preferred_maintenance_window: - description: The weekly time range, in Universal Coordinated Time (UTC), during which system maintenance can occur. - returned: success - type: str - sample: "tue:07:30-tue:08:00" -pending_modified_values: - description: A value that, if present, indicates that changes to the cluster are pending. - returned: success - type: dict - sample: {} -cluster_version: - description: The version ID of the Amazon Redshift engine that is running on the cluster. - returned: success - type: str - sample: "1.0" -allow_version_upgrade: - description: > - A Boolean value that, if true, indicates that major version upgrades will be applied - automatically to the cluster during the maintenance window. - returned: success - type: bool - sample: true|false -number_of_nodes: - description: The number of compute nodes in the cluster. - returned: success - type: int - sample: 12 -publicly_accessible: - description: A Boolean value that, if true , indicates that the cluster can be accessed from a public network. - returned: success - type: bool - sample: true|false -encrypted: - description: Boolean value that, if true , indicates that data in the cluster is encrypted at rest. - returned: success - type: bool - sample: true|false -restore_status: - description: A value that describes the status of a cluster restore action. - returned: success - type: dict - sample: {} -hsm_status: - description: > - A value that reports whether the Amazon Redshift cluster has finished applying any hardware - security module (HSM) settings changes specified in a modify cluster command. - returned: success - type: dict - sample: {} -cluster_snapshot_copy_status: - description: A value that returns the destination region and retention period that are configured for cross-region snapshot copy. - returned: success - type: dict - sample: {} -cluster_public_keys: - description: The public key for the cluster. - returned: success - type: str - sample: "ssh-rsa anjigfam Amazon-Redshift\n" -cluster_nodes: - description: The nodes in the cluster. - returned: success - type: list - sample: [ - { - "node_role": "LEADER", - "private_ip_address": "10.0.0.1", - "public_ip_address": "x.x.x.x" - }, - { - "node_role": "COMPUTE-1", - "private_ip_address": "10.0.0.3", - "public_ip_address": "x.x.x.x" - } - ] -elastic_ip_status: - description: The status of the elastic IP (EIP) address. - returned: success - type: dict - sample: {} -cluster_revision_number: - description: The specific revision number of the database in the cluster. - returned: success - type: str - sample: "1231" -tags: - description: The list of tags for the cluster. - returned: success - type: list - sample: [] -kms_key_id: - description: The AWS Key Management Service (AWS KMS) key ID of the encryption key used to encrypt data in the cluster. - returned: success - type: str - sample: "" -enhanced_vpc_routing: - description: An option that specifies whether to create the cluster with enhanced VPC routing enabled. - returned: success - type: bool - sample: true|false -iam_roles: - description: List of IAM roles attached to the cluster. - returned: success - type: list - sample: [] -''' - -import re - -try: - from botocore.exception import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -def match_tags(tags_to_match, cluster): - for key, value in tags_to_match.items(): - for tag in cluster['Tags']: - if key == tag['Key'] and value == tag['Value']: - return True - - return False - - -def find_clusters(conn, module, identifier=None, tags=None): - - try: - cluster_paginator = conn.get_paginator('describe_clusters') - clusters = cluster_paginator.paginate().build_full_result() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch clusters.') - - matched_clusters = [] - - if identifier is not None: - identifier_prog = re.compile('^' + identifier) - - for cluster in clusters['Clusters']: - - matched_identifier = True - if identifier: - matched_identifier = identifier_prog.search(cluster['ClusterIdentifier']) - - matched_tags = True - if tags: - matched_tags = match_tags(tags, cluster) - - if matched_identifier and matched_tags: - matched_clusters.append(camel_dict_to_snake_dict(cluster)) - - return matched_clusters - - -def main(): - - argument_spec = dict( - cluster_identifier=dict(type='str', aliases=['identifier', 'name']), - tags=dict(type='dict') - ) - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - if module._name == 'redshift_facts': - module.deprecate("The 'redshift_facts' module has been renamed to 'redshift_info'", version='2.13') - - cluster_identifier = module.params.get('cluster_identifier') - cluster_tags = module.params.get('tags') - - redshift = module.client('redshift') - - results = find_clusters(redshift, module, identifier=cluster_identifier, tags=cluster_tags) - module.exit_json(results=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/redshift_subnet_group.py b/lib/ansible/modules/cloud/amazon/redshift_subnet_group.py deleted file mode 100644 index 7885494af8..0000000000 --- a/lib/ansible/modules/cloud/amazon/redshift_subnet_group.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/python - -# Copyright 2014 Jens Carl, Hothead Games Inc. -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -author: - - "Jens Carl (@j-carl), Hothead Games Inc." -module: redshift_subnet_group -version_added: "2.2" -short_description: manage Redshift cluster subnet groups -description: - - Create, modifies, and deletes Redshift cluster subnet groups. -options: - state: - description: - - Specifies whether the subnet should be present or absent. - required: true - choices: ['present', 'absent' ] - type: str - group_name: - description: - - Cluster subnet group name. - required: true - aliases: ['name'] - type: str - group_description: - description: - - Database subnet group description. - aliases: ['description'] - type: str - group_subnets: - description: - - List of subnet IDs that make up the cluster subnet group. - aliases: ['subnets'] - type: list - elements: str -requirements: [ 'boto' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create a Redshift subnet group -- local_action: - module: redshift_subnet_group - state: present - group_name: redshift-subnet - group_description: Redshift subnet - group_subnets: - - 'subnet-aaaaa' - - 'subnet-bbbbb' - -# Remove subnet group -- redshift_subnet_group: - state: absent - group_name: redshift-subnet -''' - -RETURN = ''' -group: - description: dictionary containing all Redshift subnet group information - returned: success - type: complex - contains: - name: - description: name of the Redshift subnet group - returned: success - type: str - sample: "redshift_subnet_group_name" - vpc_id: - description: Id of the VPC where the subnet is located - returned: success - type: str - sample: "vpc-aabb1122" -''' - -try: - import boto - import boto.redshift - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - group_name=dict(required=True, aliases=['name']), - group_description=dict(required=False, aliases=['description']), - group_subnets=dict(required=False, aliases=['subnets'], type='list'), - )) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto v2.9.0+ required for this module') - - state = module.params.get('state') - group_name = module.params.get('group_name') - group_description = module.params.get('group_description') - group_subnets = module.params.get('group_subnets') - - if state == 'present': - for required in ('group_name', 'group_description', 'group_subnets'): - if not module.params.get(required): - module.fail_json(msg=str("parameter %s required for state='present'" % required)) - else: - for not_allowed in ('group_description', 'group_subnets'): - if module.params.get(not_allowed): - module.fail_json(msg=str("parameter %s not allowed for state='absent'" % not_allowed)) - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg=str("Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file")) - - # Connect to the Redshift endpoint. - try: - conn = connect_to_aws(boto.redshift, region, **aws_connect_params) - except boto.exception.JSONResponseError as e: - module.fail_json(msg=str(e)) - - try: - changed = False - exists = False - group = None - - try: - matching_groups = conn.describe_cluster_subnet_groups(group_name, max_records=100) - exists = len(matching_groups) > 0 - except boto.exception.JSONResponseError as e: - if e.body['Error']['Code'] != 'ClusterSubnetGroupNotFoundFault': - # if e.code != 'ClusterSubnetGroupNotFoundFault': - module.fail_json(msg=str(e)) - - if state == 'absent': - if exists: - conn.delete_cluster_subnet_group(group_name) - changed = True - - else: - if not exists: - new_group = conn.create_cluster_subnet_group(group_name, group_description, group_subnets) - group = { - 'name': new_group['CreateClusterSubnetGroupResponse']['CreateClusterSubnetGroupResult'] - ['ClusterSubnetGroup']['ClusterSubnetGroupName'], - 'vpc_id': new_group['CreateClusterSubnetGroupResponse']['CreateClusterSubnetGroupResult'] - ['ClusterSubnetGroup']['VpcId'], - } - else: - changed_group = conn.modify_cluster_subnet_group(group_name, group_subnets, description=group_description) - group = { - 'name': changed_group['ModifyClusterSubnetGroupResponse']['ModifyClusterSubnetGroupResult'] - ['ClusterSubnetGroup']['ClusterSubnetGroupName'], - 'vpc_id': changed_group['ModifyClusterSubnetGroupResponse']['ModifyClusterSubnetGroupResult'] - ['ClusterSubnetGroup']['VpcId'], - } - - changed = True - - except boto.exception.JSONResponseError as e: - module.fail_json(msg=str(e)) - - module.exit_json(changed=changed, group=group) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/route53.py b/lib/ansible/modules/cloud/amazon/route53.py deleted file mode 100644 index 6d64893700..0000000000 --- a/lib/ansible/modules/cloud/amazon/route53.py +++ /dev/null @@ -1,721 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: route53 -version_added: "1.3" -short_description: add or delete entries in Amazons Route53 DNS service -description: - - Creates and deletes DNS records in Amazons Route53 service -options: - state: - description: - - Specifies the state of the resource record. As of Ansible 2.4, the I(command) option has been changed - to I(state) as default and the choices 'present' and 'absent' have been added, but I(command) still works as well. - required: true - aliases: [ 'command' ] - choices: [ 'present', 'absent', 'get', 'create', 'delete' ] - type: str - zone: - description: - - The DNS zone to modify. - - This is a required parameter, if parameter I(hosted_zone_id) is not supplied. - type: str - hosted_zone_id: - description: - - The Hosted Zone ID of the DNS zone to modify. - - This is a required parameter, if parameter I(zone) is not supplied. - version_added: "2.0" - type: str - record: - description: - - The full DNS record to create or delete. - required: true - type: str - ttl: - description: - - The TTL, in second, to give the new record. - default: 3600 - type: int - type: - description: - - The type of DNS record to create. - required: true - choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS', 'SOA' ] - type: str - alias: - description: - - Indicates if this is an alias record. - version_added: "1.9" - type: bool - default: false - alias_hosted_zone_id: - description: - - The hosted zone identifier. - version_added: "1.9" - type: str - alias_evaluate_target_health: - description: - - Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers. - type: bool - default: false - version_added: "2.1" - value: - description: - - The new value when creating a DNS record. YAML lists or multiple comma-spaced values are allowed for non-alias records. - - When deleting a record all values for the record must be specified or Route53 will not delete it. - type: list - overwrite: - description: - - Whether an existing record should be overwritten on create if values do not match. - type: bool - retry_interval: - description: - - In the case that route53 is still servicing a prior request, this module will wait and try again after this many seconds. - If you have many domain names, the default of 500 seconds may be too long. - default: 500 - type: int - private_zone: - description: - - If set to C(yes), the private zone matching the requested name within the domain will be used if there are both public and private zones. - The default is to use the public zone. - type: bool - default: false - version_added: "1.9" - identifier: - description: - - Have to be specified for Weighted, latency-based and failover resource record sets only. - An identifier that differentiates among multiple resource record sets that have the same combination of DNS name and type. - version_added: "2.0" - type: str - weight: - description: - - Weighted resource record sets only. Among resource record sets that - have the same combination of DNS name and type, a value that - determines what portion of traffic for the current resource record set - is routed to the associated location. - version_added: "2.0" - type: int - region: - description: - - Latency-based resource record sets only Among resource record sets - that have the same combination of DNS name and type, a value that - determines which region this should be associated with for the - latency-based routing - version_added: "2.0" - type: str - health_check: - description: - - Health check to associate with this record - version_added: "2.0" - type: str - failover: - description: - - Failover resource record sets only. Whether this is the primary or - secondary resource record set. Allowed values are PRIMARY and SECONDARY - version_added: "2.0" - type: str - choices: ['SECONDARY', 'PRIMARY'] - vpc_id: - description: - - "When used in conjunction with private_zone: true, this will only modify records in the private hosted zone attached to this VPC." - - This allows you to have multiple private hosted zones, all with the same name, attached to different VPCs. - version_added: "2.0" - type: str - wait: - description: - - Wait until the changes have been replicated to all Amazon Route 53 DNS servers. - type: bool - default: false - version_added: "2.1" - wait_timeout: - description: - - How long to wait for the changes to be replicated, in seconds. - default: 300 - version_added: "2.1" - type: int -author: -- Bruce Pennypacker (@bpennypacker) -- Mike Buzzetti (@jimbydamonk) -extends_documentation_fragment: aws -''' - -RETURN = ''' -nameservers: - description: Nameservers associated with the zone. - returned: when state is 'get' - type: list - sample: - - ns-1036.awsdns-00.org. - - ns-516.awsdns-00.net. - - ns-1504.awsdns-00.co.uk. - - ns-1.awsdns-00.com. -set: - description: Info specific to the resource record. - returned: when state is 'get' - type: complex - contains: - alias: - description: Whether this is an alias. - returned: always - type: bool - sample: false - failover: - description: Whether this is the primary or secondary resource record set. - returned: always - type: str - sample: PRIMARY - health_check: - description: health_check associated with this record. - returned: always - type: str - identifier: - description: An identifier that differentiates among multiple resource record sets that have the same combination of DNS name and type. - returned: always - type: str - record: - description: Domain name for the record set. - returned: always - type: str - sample: new.foo.com. - region: - description: Which region this should be associated with for latency-based routing. - returned: always - type: str - sample: us-west-2 - ttl: - description: Resource record cache TTL. - returned: always - type: str - sample: '3600' - type: - description: Resource record set type. - returned: always - type: str - sample: A - value: - description: Record value. - returned: always - type: str - sample: 52.43.18.27 - values: - description: Record Values. - returned: always - type: list - sample: - - 52.43.18.27 - weight: - description: Weight of the record. - returned: always - type: str - sample: '3' - zone: - description: Zone this record set belongs to. - returned: always - type: str - sample: foo.bar.com. -''' - -EXAMPLES = ''' -# Add new.foo.com as an A record with 3 IPs and wait until the changes have been replicated -- route53: - state: present - zone: foo.com - record: new.foo.com - type: A - ttl: 7200 - value: 1.1.1.1,2.2.2.2,3.3.3.3 - wait: yes - -# Update new.foo.com as an A record with a list of 3 IPs and wait until the changes have been replicated -- route53: - state: present - zone: foo.com - record: new.foo.com - type: A - ttl: 7200 - value: - - 1.1.1.1 - - 2.2.2.2 - - 3.3.3.3 - wait: yes - -# Retrieve the details for new.foo.com -- route53: - state: get - zone: foo.com - record: new.foo.com - type: A - register: rec - -# Delete new.foo.com A record using the results from the get command -- route53: - state: absent - zone: foo.com - record: "{{ rec.set.record }}" - ttl: "{{ rec.set.ttl }}" - type: "{{ rec.set.type }}" - value: "{{ rec.set.value }}" - -# Add an AAAA record. Note that because there are colons in the value -# that the IPv6 address must be quoted. Also shows using the old form command=create. -- route53: - command: create - zone: foo.com - record: localhost.foo.com - type: AAAA - ttl: 7200 - value: "::1" - -# Add a SRV record with multiple fields for a service on port 22222 -# For more information on SRV records see: -# https://en.wikipedia.org/wiki/SRV_record -- route53: - state: present - zone: foo.com - record: "_example-service._tcp.foo.com" - type: SRV - value: "0 0 22222 host1.foo.com,0 0 22222 host2.foo.com" - -# Add a TXT record. Note that TXT and SPF records must be surrounded -# by quotes when sent to Route 53: -- route53: - state: present - zone: foo.com - record: localhost.foo.com - type: TXT - ttl: 7200 - value: '"bar"' - -# Add an alias record that points to an Amazon ELB: -- route53: - state: present - zone: foo.com - record: elb.foo.com - type: A - value: "{{ elb_dns_name }}" - alias: True - alias_hosted_zone_id: "{{ elb_zone_id }}" - -# Retrieve the details for elb.foo.com -- route53: - state: get - zone: foo.com - record: elb.foo.com - type: A - register: rec - -# Delete an alias record using the results from the get command -- route53: - state: absent - zone: foo.com - record: "{{ rec.set.record }}" - ttl: "{{ rec.set.ttl }}" - type: "{{ rec.set.type }}" - value: "{{ rec.set.value }}" - alias: True - alias_hosted_zone_id: "{{ rec.set.alias_hosted_zone_id }}" - -# Add an alias record that points to an Amazon ELB and evaluates it health: -- route53: - state: present - zone: foo.com - record: elb.foo.com - type: A - value: "{{ elb_dns_name }}" - alias: True - alias_hosted_zone_id: "{{ elb_zone_id }}" - alias_evaluate_target_health: True - -# Add an AAAA record with Hosted Zone ID. -- route53: - state: present - zone: foo.com - hosted_zone_id: Z2AABBCCDDEEFF - record: localhost.foo.com - type: AAAA - ttl: 7200 - value: "::1" - -# Use a routing policy to distribute traffic: -- route53: - state: present - zone: foo.com - record: www.foo.com - type: CNAME - value: host1.foo.com - ttl: 30 - # Routing policy - identifier: "host1@www" - weight: 100 - health_check: "d994b780-3150-49fd-9205-356abdd42e75" - -# Add a CAA record (RFC 6844): -- route53: - state: present - zone: example.com - record: example.com - type: CAA - value: - - 0 issue "ca.example.net" - - 0 issuewild ";" - - 0 iodef "mailto:security@example.com" - -''' - -import time -import distutils.version - -try: - import boto - import boto.ec2 - from boto.route53 import Route53Connection - from boto.route53.record import Record, ResourceRecordSets - from boto.route53.status import Status - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info - - -MINIMUM_BOTO_VERSION = '2.28.0' -WAIT_RETRY_SLEEP = 5 # how many seconds to wait between propagation status polls - - -class TimeoutError(Exception): - pass - - -def get_zone_id_by_name(conn, module, zone_name, want_private, want_vpc_id): - """Finds a zone by name or zone_id""" - for zone in invoke_with_throttling_retries(conn.get_zones): - # only save this zone id if the private status of the zone matches - # the private_zone_in boolean specified in the params - private_zone = module.boolean(zone.config.get('PrivateZone', False)) - if private_zone == want_private and zone.name == zone_name: - if want_vpc_id: - # NOTE: These details aren't available in other boto methods, hence the necessary - # extra API call - hosted_zone = invoke_with_throttling_retries(conn.get_hosted_zone, zone.id) - zone_details = hosted_zone['GetHostedZoneResponse'] - # this is to deal with this boto bug: https://github.com/boto/boto/pull/2882 - if isinstance(zone_details['VPCs'], dict): - if zone_details['VPCs']['VPC']['VPCId'] == want_vpc_id: - return zone.id - else: # Forward compatibility for when boto fixes that bug - if want_vpc_id in [v['VPCId'] for v in zone_details['VPCs']]: - return zone.id - else: - return zone.id - return None - - -def commit(changes, retry_interval, wait, wait_timeout): - """Commit changes, but retry PriorRequestNotComplete errors.""" - result = None - retry = 10 - while True: - try: - retry -= 1 - result = changes.commit() - break - except boto.route53.exception.DNSServerError as e: - code = e.body.split("<Code>")[1] - code = code.split("</Code>")[0] - if code != 'PriorRequestNotComplete' or retry < 0: - raise e - time.sleep(float(retry_interval)) - - if wait: - timeout_time = time.time() + wait_timeout - connection = changes.connection - change = result['ChangeResourceRecordSetsResponse']['ChangeInfo'] - status = Status(connection, change) - while status.status != 'INSYNC' and time.time() < timeout_time: - time.sleep(WAIT_RETRY_SLEEP) - status.update() - if time.time() >= timeout_time: - raise TimeoutError() - return result - - -# Shamelessly copied over from https://git.io/vgmDG -IGNORE_CODE = 'Throttling' -MAX_RETRIES = 5 - - -def invoke_with_throttling_retries(function_ref, *argv, **kwargs): - retries = 0 - while True: - try: - retval = function_ref(*argv, **kwargs) - return retval - except boto.exception.BotoServerError as e: - if e.code != IGNORE_CODE or retries == MAX_RETRIES: - raise e - time.sleep(5 * (2**retries)) - retries += 1 - - -def decode_name(name): - # Due to a bug in either AWS or Boto, "special" characters are returned as octals, preventing round - # tripping of things like * and @. - return name.encode().decode('unicode_escape') - - -def to_dict(rset, zone_in, zone_id): - record = dict() - record['zone'] = zone_in - record['type'] = rset.type - record['record'] = decode_name(rset.name) - record['ttl'] = str(rset.ttl) - record['identifier'] = rset.identifier - record['weight'] = rset.weight - record['region'] = rset.region - record['failover'] = rset.failover - record['health_check'] = rset.health_check - record['hosted_zone_id'] = zone_id - if rset.alias_dns_name: - record['alias'] = True - record['value'] = rset.alias_dns_name - record['values'] = [rset.alias_dns_name] - record['alias_hosted_zone_id'] = rset.alias_hosted_zone_id - record['alias_evaluate_target_health'] = rset.alias_evaluate_target_health - else: - record['alias'] = False - record['value'] = ','.join(sorted(rset.resource_records)) - record['values'] = sorted(rset.resource_records) - return record - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(type='str', required=True, choices=['absent', 'create', 'delete', 'get', 'present'], aliases=['command']), - zone=dict(type='str'), - hosted_zone_id=dict(type='str'), - record=dict(type='str', required=True), - ttl=dict(type='int', default=3600), - type=dict(type='str', required=True, choices=['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT']), - alias=dict(type='bool'), - alias_hosted_zone_id=dict(type='str'), - alias_evaluate_target_health=dict(type='bool', default=False), - value=dict(type='list'), - overwrite=dict(type='bool'), - retry_interval=dict(type='int', default=500), - private_zone=dict(type='bool', default=False), - identifier=dict(type='str'), - weight=dict(type='int'), - region=dict(type='str'), - health_check=dict(type='str'), - failover=dict(type='str', choices=['PRIMARY', 'SECONDARY']), - vpc_id=dict(type='str'), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=300), - )) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=[['zone', 'hosted_zone_id']], - # If alias is True then you must specify alias_hosted_zone as well - required_together=[['alias', 'alias_hosted_zone_id']], - # state=present, absent, create, delete THEN value is required - required_if=( - ('state', 'present', ['value']), - ('state', 'create', ['value']), - ('state', 'absent', ['value']), - ('state', 'delete', ['value']), - ), - # failover, region and weight are mutually exclusive - mutually_exclusive=[('failover', 'region', 'weight')], - # failover, region and weight require identifier - required_by=dict( - failover=('identifier',), - region=('identifier',), - weight=('identifier',), - ), - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - if distutils.version.StrictVersion(boto.__version__) < distutils.version.StrictVersion(MINIMUM_BOTO_VERSION): - module.fail_json(msg='Found boto in version %s, but >= %s is required' % (boto.__version__, MINIMUM_BOTO_VERSION)) - - if module.params['state'] in ('present', 'create'): - command_in = 'create' - elif module.params['state'] in ('absent', 'delete'): - command_in = 'delete' - elif module.params['state'] == 'get': - command_in = 'get' - - zone_in = (module.params.get('zone') or '').lower() - hosted_zone_id_in = module.params.get('hosted_zone_id') - ttl_in = module.params.get('ttl') - record_in = module.params.get('record').lower() - type_in = module.params.get('type') - value_in = module.params.get('value') or [] - alias_in = module.params.get('alias') - alias_hosted_zone_id_in = module.params.get('alias_hosted_zone_id') - alias_evaluate_target_health_in = module.params.get('alias_evaluate_target_health') - retry_interval_in = module.params.get('retry_interval') - - if module.params['vpc_id'] is not None: - private_zone_in = True - else: - private_zone_in = module.params.get('private_zone') - - identifier_in = module.params.get('identifier') - weight_in = module.params.get('weight') - region_in = module.params.get('region') - health_check_in = module.params.get('health_check') - failover_in = module.params.get('failover') - vpc_id_in = module.params.get('vpc_id') - wait_in = module.params.get('wait') - wait_timeout_in = module.params.get('wait_timeout') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - if zone_in[-1:] != '.': - zone_in += "." - - if record_in[-1:] != '.': - record_in += "." - - if command_in == 'create' or command_in == 'delete': - if alias_in and len(value_in) != 1: - module.fail_json(msg="parameter 'value' must contain a single dns name for alias records") - if (weight_in is None and region_in is None and failover_in is None) and identifier_in is not None: - module.fail_json(msg="You have specified identifier which makes sense only if you specify one of: weight, region or failover.") - - # connect to the route53 endpoint - try: - conn = Route53Connection(**aws_connect_kwargs) - except boto.exception.BotoServerError as e: - module.fail_json(msg=e.error_message) - - # Find the named zone ID - zone_id = hosted_zone_id_in or get_zone_id_by_name(conn, module, zone_in, private_zone_in, vpc_id_in) - - # Verify that the requested zone is already defined in Route53 - if zone_id is None: - errmsg = "Zone %s does not exist in Route53" % (zone_in or hosted_zone_id_in) - module.fail_json(msg=errmsg) - - record = {} - - found_record = False - wanted_rset = Record(name=record_in, type=type_in, ttl=ttl_in, - identifier=identifier_in, weight=weight_in, - region=region_in, health_check=health_check_in, - failover=failover_in) - for v in value_in: - if alias_in: - wanted_rset.set_alias(alias_hosted_zone_id_in, v, alias_evaluate_target_health_in) - else: - wanted_rset.add_value(v) - - need_to_sort_records = (type_in == 'CAA') - - # Sort records for wanted_rset if necessary (keep original list) - unsorted_records = wanted_rset.resource_records - if need_to_sort_records: - wanted_rset.resource_records = sorted(unsorted_records) - - sets = invoke_with_throttling_retries(conn.get_all_rrsets, zone_id, name=record_in, - type=type_in, identifier=identifier_in) - sets_iter = iter(sets) - while True: - try: - rset = invoke_with_throttling_retries(next, sets_iter) - except StopIteration: - break - # Need to save this changes in rset, because of comparing rset.to_xml() == wanted_rset.to_xml() in next block - rset.name = decode_name(rset.name) - - if identifier_in is not None: - identifier_in = str(identifier_in) - - if rset.type == type_in and rset.name.lower() == record_in.lower() and rset.identifier == identifier_in: - if need_to_sort_records: - # Sort records - rset.resource_records = sorted(rset.resource_records) - found_record = True - record = to_dict(rset, zone_in, zone_id) - if command_in == 'create' and rset.to_xml() == wanted_rset.to_xml(): - module.exit_json(changed=False) - - # We need to look only at the first rrset returned by the above call, - # so break here. The returned elements begin with the one matching our - # requested name, type, and identifier, if such an element exists, - # followed by all others that come after it in alphabetical order. - # Therefore, if the first set does not match, no subsequent set will - # match either. - break - - if command_in == 'get': - if type_in == 'NS': - ns = record.get('values', []) - else: - # Retrieve name servers associated to the zone. - z = invoke_with_throttling_retries(conn.get_zone, zone_in) - ns = invoke_with_throttling_retries(z.get_nameservers) - - module.exit_json(changed=False, set=record, nameservers=ns) - - if command_in == 'delete' and not found_record: - module.exit_json(changed=False) - - changes = ResourceRecordSets(conn, zone_id) - - if command_in == 'create' or command_in == 'delete': - if command_in == 'create' and found_record: - if not module.params['overwrite']: - module.fail_json(msg="Record already exists with different value. Set 'overwrite' to replace it") - command = 'UPSERT' - else: - command = command_in.upper() - # Restore original order of records - wanted_rset.resource_records = unsorted_records - changes.add_change_record(command, wanted_rset) - - if not module.check_mode: - try: - invoke_with_throttling_retries(commit, changes, retry_interval_in, wait_in, wait_timeout_in) - except boto.route53.exception.DNSServerError as e: - txt = e.body.split("<Message>")[1] - txt = txt.split("</Message>")[0] - if "but it already exists" in txt: - module.exit_json(changed=False) - else: - module.fail_json(msg=txt) - except TimeoutError: - module.fail_json(msg='Timeout waiting for changes to replicate') - - module.exit_json( - changed=True, - diff=dict( - before=record, - after=to_dict(wanted_rset, zone_in, zone_id) if command != 'delete' else {}, - ), - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/route53_health_check.py b/lib/ansible/modules/cloud/amazon/route53_health_check.py deleted file mode 100644 index 0eb30e16cc..0000000000 --- a/lib/ansible/modules/cloud/amazon/route53_health_check.py +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: route53_health_check -short_description: Add or delete health-checks in Amazons Route53 DNS service -description: - - Creates and deletes DNS Health checks in Amazons Route53 service. - - Only the port, resource_path, string_match and request_interval are - considered when updating existing health-checks. -version_added: "2.0" -options: - state: - description: - - Specifies the action to take. - choices: [ 'present', 'absent' ] - type: str - default: 'present' - ip_address: - description: - - IP address of the end-point to check. Either this or I(fqdn) has to be provided. - type: str - port: - description: - - The port on the endpoint on which you want Amazon Route 53 to perform - health checks. Required for TCP checks. - type: int - type: - description: - - The type of health check that you want to create, which indicates how - Amazon Route 53 determines whether an endpoint is healthy. - required: true - choices: [ 'HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP' ] - type: str - resource_path: - description: - - The path that you want Amazon Route 53 to request when performing - health checks. The path can be any value for which your endpoint will - return an HTTP status code of 2xx or 3xx when the endpoint is healthy, - for example the file /docs/route53-health-check.html. - - Required for all checks except TCP. - - The path must begin with a / - - Maximum 255 characters. - type: str - fqdn: - description: - - Domain name of the endpoint to check. Either this or I(ip_address) has - to be provided. When both are given the `fqdn` is used in the `Host:` - header of the HTTP request. - type: str - string_match: - description: - - If the check type is HTTP_STR_MATCH or HTTP_STR_MATCH, the string - that you want Amazon Route 53 to search for in the response body from - the specified resource. If the string appears in the first 5120 bytes - of the response body, Amazon Route 53 considers the resource healthy. - type: str - request_interval: - description: - - The number of seconds between the time that Amazon Route 53 gets a - response from your endpoint and the time that it sends the next - health-check request. - default: 30 - choices: [ 10, 30 ] - type: int - failure_threshold: - description: - - The number of consecutive health checks that an endpoint must pass or - fail for Amazon Route 53 to change the current status of the endpoint - from unhealthy to healthy or vice versa. - default: 3 - choices: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] - type: int -author: "zimbatm (@zimbatm)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Create a health-check for host1.example.com and use it in record -- route53_health_check: - state: present - fqdn: host1.example.com - type: HTTP_STR_MATCH - resource_path: / - string_match: "Hello" - request_interval: 10 - failure_threshold: 2 - register: my_health_check - -- route53: - action: create - zone: "example.com" - type: CNAME - record: "www.example.com" - value: host1.example.com - ttl: 30 - # Routing policy - identifier: "host1@www" - weight: 100 - health_check: "{{ my_health_check.health_check.id }}" - -# Delete health-check -- route53_health_check: - state: absent - fqdn: host1.example.com - -''' - -import uuid - -try: - import boto - import boto.ec2 - from boto import route53 - from boto.route53 import Route53Connection, exception - from boto.route53.healthcheck import HealthCheck - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -# import module snippets -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info - - -# Things that can't get changed: -# protocol -# ip_address or domain -# request_interval -# string_match if not previously enabled -def find_health_check(conn, wanted): - """Searches for health checks that have the exact same set of immutable values""" - - results = conn.get_list_health_checks() - - while True: - for check in results.HealthChecks: - config = check.HealthCheckConfig - if ( - config.get('IPAddress') == wanted.ip_addr and - config.get('FullyQualifiedDomainName') == wanted.fqdn and - config.get('Type') == wanted.hc_type and - config.get('RequestInterval') == str(wanted.request_interval) and - config.get('Port') == str(wanted.port) - ): - return check - - if (results.IsTruncated == 'true'): - results = conn.get_list_health_checks(marker=results.NextMarker) - else: - return None - - -def to_health_check(config): - return HealthCheck( - config.get('IPAddress'), - int(config.get('Port')), - config.get('Type'), - config.get('ResourcePath'), - fqdn=config.get('FullyQualifiedDomainName'), - string_match=config.get('SearchString'), - request_interval=int(config.get('RequestInterval')), - failure_threshold=int(config.get('FailureThreshold')), - ) - - -def health_check_diff(a, b): - a = a.__dict__ - b = b.__dict__ - if a == b: - return {} - diff = {} - for key in set(a.keys()) | set(b.keys()): - if a.get(key) != b.get(key): - diff[key] = b.get(key) - return diff - - -def to_template_params(health_check): - params = { - 'ip_addr_part': '', - 'port': health_check.port, - 'type': health_check.hc_type, - 'resource_path_part': '', - 'fqdn_part': '', - 'string_match_part': '', - 'request_interval': health_check.request_interval, - 'failure_threshold': health_check.failure_threshold, - } - if health_check.ip_addr: - params['ip_addr_part'] = HealthCheck.XMLIpAddrPart % {'ip_addr': health_check.ip_addr} - if health_check.resource_path: - params['resource_path_part'] = XMLResourcePathPart % {'resource_path': health_check.resource_path} - if health_check.fqdn: - params['fqdn_part'] = HealthCheck.XMLFQDNPart % {'fqdn': health_check.fqdn} - if health_check.string_match: - params['string_match_part'] = HealthCheck.XMLStringMatchPart % {'string_match': health_check.string_match} - return params - - -XMLResourcePathPart = """<ResourcePath>%(resource_path)s</ResourcePath>""" - -POSTXMLBody = """ - <CreateHealthCheckRequest xmlns="%(xmlns)s"> - <CallerReference>%(caller_ref)s</CallerReference> - <HealthCheckConfig> - %(ip_addr_part)s - <Port>%(port)s</Port> - <Type>%(type)s</Type> - %(resource_path_part)s - %(fqdn_part)s - %(string_match_part)s - <RequestInterval>%(request_interval)s</RequestInterval> - <FailureThreshold>%(failure_threshold)s</FailureThreshold> - </HealthCheckConfig> - </CreateHealthCheckRequest> - """ - -UPDATEHCXMLBody = """ - <UpdateHealthCheckRequest xmlns="%(xmlns)s"> - <HealthCheckVersion>%(health_check_version)s</HealthCheckVersion> - %(ip_addr_part)s - <Port>%(port)s</Port> - %(resource_path_part)s - %(fqdn_part)s - %(string_match_part)s - <FailureThreshold>%(failure_threshold)i</FailureThreshold> - </UpdateHealthCheckRequest> - """ - - -def create_health_check(conn, health_check, caller_ref=None): - if caller_ref is None: - caller_ref = str(uuid.uuid4()) - uri = '/%s/healthcheck' % conn.Version - params = to_template_params(health_check) - params.update(xmlns=conn.XMLNameSpace, caller_ref=caller_ref) - - xml_body = POSTXMLBody % params - response = conn.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body) - body = response.read() - boto.log.debug(body) - if response.status == 201: - e = boto.jsonresponse.Element() - h = boto.jsonresponse.XmlHandler(e, None) - h.parse(body) - return e - else: - raise exception.DNSServerError(response.status, response.reason, body) - - -def update_health_check(conn, health_check_id, health_check_version, health_check): - uri = '/%s/healthcheck/%s' % (conn.Version, health_check_id) - params = to_template_params(health_check) - params.update( - xmlns=conn.XMLNameSpace, - health_check_version=health_check_version, - ) - xml_body = UPDATEHCXMLBody % params - response = conn.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body) - body = response.read() - boto.log.debug(body) - if response.status not in (200, 204): - raise exception.DNSServerError(response.status, - response.reason, - body) - e = boto.jsonresponse.Element() - h = boto.jsonresponse.XmlHandler(e, None) - h.parse(body) - return e - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state=dict(choices=['present', 'absent'], default='present'), - ip_address=dict(), - port=dict(type='int'), - type=dict(required=True, choices=['HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP']), - resource_path=dict(), - fqdn=dict(), - string_match=dict(), - request_interval=dict(type='int', choices=[10, 30], default=30), - failure_threshold=dict(type='int', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default=3), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto 2.27.0+ required for this module') - - state_in = module.params.get('state') - ip_addr_in = module.params.get('ip_address') - port_in = module.params.get('port') - type_in = module.params.get('type') - resource_path_in = module.params.get('resource_path') - fqdn_in = module.params.get('fqdn') - string_match_in = module.params.get('string_match') - request_interval_in = module.params.get('request_interval') - failure_threshold_in = module.params.get('failure_threshold') - - if ip_addr_in is None and fqdn_in is None: - module.fail_json(msg="parameter 'ip_address' or 'fqdn' is required") - - # Default port - if port_in is None: - if type_in in ['HTTP', 'HTTP_STR_MATCH']: - port_in = 80 - elif type_in in ['HTTPS', 'HTTPS_STR_MATCH']: - port_in = 443 - else: - module.fail_json(msg="parameter 'port' is required for 'type' TCP") - - # string_match in relation with type - if type_in in ['HTTP_STR_MATCH', 'HTTPS_STR_MATCH']: - if string_match_in is None: - module.fail_json(msg="parameter 'string_match' is required for the HTTP(S)_STR_MATCH types") - elif len(string_match_in) > 255: - module.fail_json(msg="parameter 'string_match' is limited to 255 characters max") - elif string_match_in: - module.fail_json(msg="parameter 'string_match' argument is only for the HTTP(S)_STR_MATCH types") - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - # connect to the route53 endpoint - try: - conn = Route53Connection(**aws_connect_kwargs) - except boto.exception.BotoServerError as e: - module.fail_json(msg=e.error_message) - - changed = False - action = None - check_id = None - wanted_config = HealthCheck(ip_addr_in, port_in, type_in, resource_path_in, fqdn_in, string_match_in, request_interval_in, failure_threshold_in) - existing_check = find_health_check(conn, wanted_config) - if existing_check: - check_id = existing_check.Id - existing_config = to_health_check(existing_check.HealthCheckConfig) - - if state_in == 'present': - if existing_check is None: - action = "create" - check_id = create_health_check(conn, wanted_config).HealthCheck.Id - changed = True - else: - diff = health_check_diff(existing_config, wanted_config) - if diff: - action = "update" - update_health_check(conn, existing_check.Id, int(existing_check.HealthCheckVersion), wanted_config) - changed = True - elif state_in == 'absent': - if check_id: - action = "delete" - conn.delete_health_check(check_id) - changed = True - else: - module.fail_json(msg="Logic Error: Unknown state") - - module.exit_json(changed=changed, health_check=dict(id=check_id), action=action) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/route53_info.py b/lib/ansible/modules/cloud/amazon/route53_info.py deleted file mode 100644 index 910bdd1188..0000000000 --- a/lib/ansible/modules/cloud/amazon/route53_info.py +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: route53_info -short_description: Retrieves route53 details using AWS methods -description: - - Gets various details related to Route53 zone, record set or health check details. - - This module was called C(route53_facts) before Ansible 2.9. The usage did not change. -version_added: "2.0" -options: - query: - description: - - Specifies the query action to take. - required: True - choices: [ - 'change', - 'checker_ip_range', - 'health_check', - 'hosted_zone', - 'record_sets', - 'reusable_delegation_set', - ] - type: str - change_id: - description: - - The ID of the change batch request. - - The value that you specify here is the value that - ChangeResourceRecordSets returned in the Id element - when you submitted the request. - - Required if I(query=change). - required: false - type: str - hosted_zone_id: - description: - - The Hosted Zone ID of the DNS zone. - - Required if I(query) is set to I(hosted_zone) and I(hosted_zone_method) is set to I(details). - - Required if I(query) is set to I(record_sets). - required: false - type: str - max_items: - description: - - Maximum number of items to return for various get/list requests. - required: false - type: str - next_marker: - description: - - "Some requests such as list_command: hosted_zones will return a maximum - number of entries - EG 100 or the number specified by I(max_items). - If the number of entries exceeds this maximum another request can be sent - using the NextMarker entry from the first response to get the next page - of results." - required: false - type: str - delegation_set_id: - description: - - The DNS Zone delegation set ID. - required: false - type: str - start_record_name: - description: - - "The first name in the lexicographic ordering of domain names that you want - the list_command: record_sets to start listing from." - required: false - type: str - type: - description: - - The type of DNS record. - required: false - choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS' ] - type: str - dns_name: - description: - - The first name in the lexicographic ordering of domain names that you want - the list_command to start listing from. - required: false - type: str - resource_id: - description: - - The ID/s of the specified resource/s. - - Required if I(query=health_check) and I(health_check_method=tags). - - Required if I(query=hosted_zone) and I(hosted_zone_method=tags). - required: false - aliases: ['resource_ids'] - type: list - elements: str - health_check_id: - description: - - The ID of the health check. - - Required if C(query) is set to C(health_check) and - C(health_check_method) is set to C(details) or C(status) or C(failure_reason). - required: false - type: str - hosted_zone_method: - description: - - "This is used in conjunction with query: hosted_zone. - It allows for listing details, counts or tags of various - hosted zone details." - required: false - choices: [ - 'details', - 'list', - 'list_by_name', - 'count', - 'tags', - ] - default: 'list' - type: str - health_check_method: - description: - - "This is used in conjunction with query: health_check. - It allows for listing details, counts or tags of various - health check details." - required: false - choices: [ - 'list', - 'details', - 'status', - 'failure_reason', - 'count', - 'tags', - ] - default: 'list' - type: str -author: Karen Cheng (@Etherdaemon) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple example of listing all hosted zones -- name: List all hosted zones - route53_info: - query: hosted_zone - register: hosted_zones - -# Getting a count of hosted zones -- name: Return a count of all hosted zones - route53_info: - query: hosted_zone - hosted_zone_method: count - register: hosted_zone_count - -- name: List the first 20 resource record sets in a given hosted zone - route53_info: - profile: account_name - query: record_sets - hosted_zone_id: ZZZ1111112222 - max_items: 20 - register: record_sets - -- name: List first 20 health checks - route53_info: - query: health_check - health_check_method: list - max_items: 20 - register: health_checks - -- name: Get health check last failure_reason - route53_info: - query: health_check - health_check_method: failure_reason - health_check_id: 00000000-1111-2222-3333-12345678abcd - register: health_check_failure_reason - -- name: Retrieve reusable delegation set details - route53_info: - query: reusable_delegation_set - delegation_set_id: delegation id - register: delegation_sets - -- name: setup of example for using next_marker - route53_info: - query: hosted_zone - max_items: 1 - register: first_info - -- name: example for using next_marker - route53_info: - query: hosted_zone - next_marker: "{{ first_info.NextMarker }}" - max_items: 1 - when: "{{ 'NextMarker' in first_info }}" - -- name: retrieve host entries starting with host1.workshop.test.io - block: - - name: grab zone id - route53_zone: - zone: "test.io" - register: AWSINFO - - - name: grab Route53 record information - route53_info: - type: A - query: record_sets - hosted_zone_id: "{{ AWSINFO.zone_id }}" - start_record_name: "host1.workshop.test.io" - register: RECORDS -''' -try: - import boto - import botocore - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils._text import to_native - - -def get_hosted_zone(client, module): - params = dict() - - if module.params.get('hosted_zone_id'): - params['Id'] = module.params.get('hosted_zone_id') - else: - module.fail_json(msg="Hosted Zone Id is required") - - return client.get_hosted_zone(**params) - - -def reusable_delegation_set_details(client, module): - params = dict() - if not module.params.get('delegation_set_id'): - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - results = client.list_reusable_delegation_sets(**params) - else: - params['DelegationSetId'] = module.params.get('delegation_set_id') - results = client.get_reusable_delegation_set(**params) - - return results - - -def list_hosted_zones(client, module): - params = dict() - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - if module.params.get('delegation_set_id'): - params['DelegationSetId'] = module.params.get('delegation_set_id') - - paginator = client.get_paginator('list_hosted_zones') - zones = paginator.paginate(**params).build_full_result()['HostedZones'] - return { - "HostedZones": zones, - "list": zones, - } - - -def list_hosted_zones_by_name(client, module): - params = dict() - - if module.params.get('hosted_zone_id'): - params['HostedZoneId'] = module.params.get('hosted_zone_id') - - if module.params.get('dns_name'): - params['DNSName'] = module.params.get('dns_name') - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - return client.list_hosted_zones_by_name(**params) - - -def change_details(client, module): - params = dict() - - if module.params.get('change_id'): - params['Id'] = module.params.get('change_id') - else: - module.fail_json(msg="change_id is required") - - results = client.get_change(**params) - return results - - -def checker_ip_range_details(client, module): - return client.get_checker_ip_ranges() - - -def get_count(client, module): - if module.params.get('query') == 'health_check': - results = client.get_health_check_count() - else: - results = client.get_hosted_zone_count() - - return results - - -def get_health_check(client, module): - params = dict() - - if not module.params.get('health_check_id'): - module.fail_json(msg="health_check_id is required") - else: - params['HealthCheckId'] = module.params.get('health_check_id') - - if module.params.get('health_check_method') == 'details': - results = client.get_health_check(**params) - elif module.params.get('health_check_method') == 'failure_reason': - results = client.get_health_check_last_failure_reason(**params) - elif module.params.get('health_check_method') == 'status': - results = client.get_health_check_status(**params) - - return results - - -def get_resource_tags(client, module): - params = dict() - - if module.params.get('resource_id'): - params['ResourceIds'] = module.params.get('resource_id') - else: - module.fail_json(msg="resource_id or resource_ids is required") - - if module.params.get('query') == 'health_check': - params['ResourceType'] = 'healthcheck' - else: - params['ResourceType'] = 'hostedzone' - - return client.list_tags_for_resources(**params) - - -def list_health_checks(client, module): - params = dict() - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('next_marker'): - params['Marker'] = module.params.get('next_marker') - - paginator = client.get_paginator('list_health_checks') - health_checks = paginator.paginate(**params).build_full_result()['HealthChecks'] - return { - "HealthChecks": health_checks, - "list": health_checks, - } - - -def record_sets_details(client, module): - params = dict() - - if module.params.get('hosted_zone_id'): - params['HostedZoneId'] = module.params.get('hosted_zone_id') - else: - module.fail_json(msg="Hosted Zone Id is required") - - if module.params.get('max_items'): - params['MaxItems'] = module.params.get('max_items') - - if module.params.get('start_record_name'): - params['StartRecordName'] = module.params.get('start_record_name') - - if module.params.get('type') and not module.params.get('start_record_name'): - module.fail_json(msg="start_record_name must be specified if type is set") - elif module.params.get('type'): - params['StartRecordType'] = module.params.get('type') - - paginator = client.get_paginator('list_resource_record_sets') - record_sets = paginator.paginate(**params).build_full_result()['ResourceRecordSets'] - return { - "ResourceRecordSets": record_sets, - "list": record_sets, - } - - -def health_check_details(client, module): - health_check_invocations = { - 'list': list_health_checks, - 'details': get_health_check, - 'status': get_health_check, - 'failure_reason': get_health_check, - 'count': get_count, - 'tags': get_resource_tags, - } - - results = health_check_invocations[module.params.get('health_check_method')](client, module) - return results - - -def hosted_zone_details(client, module): - hosted_zone_invocations = { - 'details': get_hosted_zone, - 'list': list_hosted_zones, - 'list_by_name': list_hosted_zones_by_name, - 'count': get_count, - 'tags': get_resource_tags, - } - - results = hosted_zone_invocations[module.params.get('hosted_zone_method')](client, module) - return results - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - query=dict(choices=[ - 'change', - 'checker_ip_range', - 'health_check', - 'hosted_zone', - 'record_sets', - 'reusable_delegation_set', - ], required=True), - change_id=dict(), - hosted_zone_id=dict(), - max_items=dict(), - next_marker=dict(), - delegation_set_id=dict(), - start_record_name=dict(), - type=dict(choices=[ - 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS' - ]), - dns_name=dict(), - resource_id=dict(type='list', aliases=['resource_ids']), - health_check_id=dict(), - hosted_zone_method=dict(choices=[ - 'details', - 'list', - 'list_by_name', - 'count', - 'tags' - ], default='list'), - health_check_method=dict(choices=[ - 'list', - 'details', - 'status', - 'failure_reason', - 'count', - 'tags', - ], default='list'), - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ - ['hosted_zone_method', 'health_check_method'], - ], - ) - if module._name == 'route53_facts': - module.deprecate("The 'route53_facts' module has been renamed to 'route53_info'", version='2.13') - - # Validate Requirements - if not (HAS_BOTO or HAS_BOTO3): - module.fail_json(msg='json and boto/boto3 is required.') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - route53 = boto3_conn(module, conn_type='client', resource='route53', region=region, endpoint=ec2_url, **aws_connect_kwargs) - - invocations = { - 'change': change_details, - 'checker_ip_range': checker_ip_range_details, - 'health_check': health_check_details, - 'hosted_zone': hosted_zone_details, - 'record_sets': record_sets_details, - 'reusable_delegation_set': reusable_delegation_set_details, - } - - results = dict(changed=False) - try: - results = invocations[module.params.get('query')](route53, module) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg=to_native(e)) - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/route53_zone.py b/lib/ansible/modules/cloud/amazon/route53_zone.py deleted file mode 100644 index f3285da2c5..0000000000 --- a/lib/ansible/modules/cloud/amazon/route53_zone.py +++ /dev/null @@ -1,442 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' -module: route53_zone -short_description: add or delete Route53 zones -description: - - Creates and deletes Route53 private and public zones. -version_added: "2.0" -requirements: [ boto3 ] -options: - zone: - description: - - "The DNS zone record (eg: foo.com.)" - required: true - type: str - state: - description: - - Whether or not the zone should exist or not. - default: present - choices: [ "present", "absent" ] - type: str - vpc_id: - description: - - The VPC ID the zone should be a part of (if this is going to be a private zone). - type: str - vpc_region: - description: - - The VPC Region the zone should be a part of (if this is going to be a private zone). - type: str - comment: - description: - - Comment associated with the zone. - default: '' - type: str - hosted_zone_id: - description: - - The unique zone identifier you want to delete or "all" if there are many zones with the same domain name. - - Required if there are multiple zones identified with the above options. - version_added: 2.4 - type: str - delegation_set_id: - description: - - The reusable delegation set ID to be associated with the zone. - - Note that you can't associate a reusable delegation set with a private hosted zone. - version_added: 2.6 - type: str -extends_documentation_fragment: - - aws - - ec2 -author: "Christopher Troup (@minichate)" -''' - -EXAMPLES = ''' -- name: create a public zone - route53_zone: - zone: example.com - comment: this is an example - -- name: delete a public zone - route53_zone: - zone: example.com - state: absent - -- name: create a private zone - route53_zone: - zone: devel.example.com - vpc_id: '{{ myvpc_id }}' - vpc_region: us-west-2 - comment: developer domain - -- name: create a public zone associated with a specific reusable delegation set - route53_zone: - zone: example.com - comment: reusable delegation set example - delegation_set_id: A1BCDEF2GHIJKL -''' - -RETURN = ''' -comment: - description: optional hosted zone comment - returned: when hosted zone exists - type: str - sample: "Private zone" -name: - description: hosted zone name - returned: when hosted zone exists - type: str - sample: "private.local." -private_zone: - description: whether hosted zone is private or public - returned: when hosted zone exists - type: bool - sample: true -vpc_id: - description: id of vpc attached to private hosted zone - returned: for private hosted zone - type: str - sample: "vpc-1d36c84f" -vpc_region: - description: region of vpc attached to private hosted zone - returned: for private hosted zone - type: str - sample: "eu-west-1" -zone_id: - description: hosted zone id - returned: when hosted zone exists - type: str - sample: "Z6JQG9820BEFMW" -delegation_set_id: - description: id of the associated reusable delegation set - returned: for public hosted zones, if they have been associated with a reusable delegation set - type: str - sample: "A1BCDEF2GHIJKL" -''' - -import time -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def find_zones(module, client, zone_in, private_zone): - try: - paginator = client.get_paginator('list_hosted_zones') - results = paginator.paginate().build_full_result() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not list current hosted zones") - zones = [] - for r53zone in results['HostedZones']: - if r53zone['Name'] != zone_in: - continue - # only save zone names that match the public/private setting - if (r53zone['Config']['PrivateZone'] and private_zone) or \ - (not r53zone['Config']['PrivateZone'] and not private_zone): - zones.append(r53zone) - - return zones - - -def create(module, client, matching_zones): - zone_in = module.params.get('zone').lower() - vpc_id = module.params.get('vpc_id') - vpc_region = module.params.get('vpc_region') - comment = module.params.get('comment') - delegation_set_id = module.params.get('delegation_set_id') - - if not zone_in.endswith('.'): - zone_in += "." - - private_zone = bool(vpc_id and vpc_region) - - record = { - 'private_zone': private_zone, - 'vpc_id': vpc_id, - 'vpc_region': vpc_region, - 'comment': comment, - 'name': zone_in, - 'delegation_set_id': delegation_set_id, - 'zone_id': None, - } - - if private_zone: - changed, result = create_or_update_private(module, client, matching_zones, record) - else: - changed, result = create_or_update_public(module, client, matching_zones, record) - - return changed, result - - -def create_or_update_private(module, client, matching_zones, record): - for z in matching_zones: - try: - result = client.get_hosted_zone(Id=z['Id']) # could be in different regions or have different VPCids - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % z['Id']) - zone_details = result['HostedZone'] - vpc_details = result['VPCs'] - current_vpc_id = None - current_vpc_region = None - if isinstance(vpc_details, dict): - if vpc_details['VPC']['VPCId'] == record['vpc_id']: - current_vpc_id = vpc_details['VPC']['VPCId'] - current_vpc_region = vpc_details['VPC']['VPCRegion'] - else: - if record['vpc_id'] in [v['VPCId'] for v in vpc_details]: - current_vpc_id = record['vpc_id'] - if record['vpc_region'] in [v['VPCRegion'] for v in vpc_details]: - current_vpc_region = record['vpc_region'] - - if record['vpc_id'] == current_vpc_id and record['vpc_region'] == current_vpc_region: - record['zone_id'] = zone_details['Id'].replace('/hostedzone/', '') - if 'Comment' in zone_details['Config'] and zone_details['Config']['Comment'] != record['comment']: - if not module.check_mode: - try: - client.update_hosted_zone_comment(Id=zone_details['Id'], Comment=record['comment']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not update comment for hosted zone %s" % zone_details['Id']) - return True, record - else: - record['msg'] = "There is already a private hosted zone in the same region with the same VPC \ - you chose. Unable to create a new private hosted zone in the same name space." - return False, record - - if not module.check_mode: - try: - result = client.create_hosted_zone( - Name=record['name'], - HostedZoneConfig={ - 'Comment': record['comment'] if record['comment'] is not None else "", - 'PrivateZone': True, - }, - VPC={ - 'VPCRegion': record['vpc_region'], - 'VPCId': record['vpc_id'], - }, - CallerReference="%s-%s" % (record['name'], time.time()), - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not create hosted zone") - - hosted_zone = result['HostedZone'] - zone_id = hosted_zone['Id'].replace('/hostedzone/', '') - record['zone_id'] = zone_id - - changed = True - return changed, record - - -def create_or_update_public(module, client, matching_zones, record): - zone_details, zone_delegation_set_details = None, {} - for matching_zone in matching_zones: - try: - zone = client.get_hosted_zone(Id=matching_zone['Id']) - zone_details = zone['HostedZone'] - zone_delegation_set_details = zone.get('DelegationSet', {}) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % matching_zone['Id']) - if 'Comment' in zone_details['Config'] and zone_details['Config']['Comment'] != record['comment']: - if not module.check_mode: - try: - client.update_hosted_zone_comment( - Id=zone_details['Id'], - Comment=record['comment'] - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not update comment for hosted zone %s" % zone_details['Id']) - changed = True - else: - changed = False - break - - if zone_details is None: - if not module.check_mode: - try: - params = dict( - Name=record['name'], - HostedZoneConfig={ - 'Comment': record['comment'] if record['comment'] is not None else "", - 'PrivateZone': False, - }, - CallerReference="%s-%s" % (record['name'], time.time()), - ) - - if record.get('delegation_set_id') is not None: - params['DelegationSetId'] = record['delegation_set_id'] - - result = client.create_hosted_zone(**params) - zone_details = result['HostedZone'] - zone_delegation_set_details = result.get('DelegationSet', {}) - - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not create hosted zone") - changed = True - - if module.check_mode: - if zone_details: - record['zone_id'] = zone_details['Id'].replace('/hostedzone/', '') - else: - record['zone_id'] = zone_details['Id'].replace('/hostedzone/', '') - record['name'] = zone_details['Name'] - record['delegation_set_id'] = zone_delegation_set_details.get('Id', '').replace('/delegationset/', '') - - return changed, record - - -def delete_private(module, client, matching_zones, vpc_id, vpc_region): - for z in matching_zones: - try: - result = client.get_hosted_zone(Id=z['Id']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % z['Id']) - zone_details = result['HostedZone'] - vpc_details = result['VPCs'] - if isinstance(vpc_details, dict): - if vpc_details['VPC']['VPCId'] == vpc_id and vpc_region == vpc_details['VPC']['VPCRegion']: - if not module.check_mode: - try: - client.delete_hosted_zone(Id=z['Id']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id']) - return True, "Successfully deleted %s" % zone_details['Name'] - else: - if vpc_id in [v['VPCId'] for v in vpc_details] and vpc_region in [v['VPCRegion'] for v in vpc_details]: - if not module.check_mode: - try: - client.delete_hosted_zone(Id=z['Id']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id']) - return True, "Successfully deleted %s" % zone_details['Name'] - - return False, "The vpc_id and the vpc_region do not match a private hosted zone." - - -def delete_public(module, client, matching_zones): - if len(matching_zones) > 1: - changed = False - msg = "There are multiple zones that match. Use hosted_zone_id to specify the correct zone." - else: - if not module.check_mode: - try: - client.delete_hosted_zone(Id=matching_zones[0]['Id']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not get delete hosted zone %s" % matching_zones[0]['Id']) - changed = True - msg = "Successfully deleted %s" % matching_zones[0]['Id'] - return changed, msg - - -def delete_hosted_id(module, client, hosted_zone_id, matching_zones): - if hosted_zone_id == "all": - deleted = [] - for z in matching_zones: - deleted.append(z['Id']) - if not module.check_mode: - try: - client.delete_hosted_zone(Id=z['Id']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id']) - changed = True - msg = "Successfully deleted zones: %s" % deleted - elif hosted_zone_id in [zo['Id'].replace('/hostedzone/', '') for zo in matching_zones]: - if not module.check_mode: - try: - client.delete_hosted_zone(Id=hosted_zone_id) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Could not delete hosted zone %s" % hosted_zone_id) - changed = True - msg = "Successfully deleted zone: %s" % hosted_zone_id - else: - changed = False - msg = "There is no zone to delete that matches hosted_zone_id %s." % hosted_zone_id - return changed, msg - - -def delete(module, client, matching_zones): - zone_in = module.params.get('zone').lower() - vpc_id = module.params.get('vpc_id') - vpc_region = module.params.get('vpc_region') - hosted_zone_id = module.params.get('hosted_zone_id') - - if not zone_in.endswith('.'): - zone_in += "." - - private_zone = bool(vpc_id and vpc_region) - - if zone_in in [z['Name'] for z in matching_zones]: - if hosted_zone_id: - changed, result = delete_hosted_id(module, client, hosted_zone_id, matching_zones) - else: - if private_zone: - changed, result = delete_private(module, client, matching_zones, vpc_id, vpc_region) - else: - changed, result = delete_public(module, client, matching_zones) - else: - changed = False - result = "No zone to delete." - - return changed, result - - -def main(): - argument_spec = dict( - zone=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - vpc_id=dict(default=None), - vpc_region=dict(default=None), - comment=dict(default=''), - hosted_zone_id=dict(), - delegation_set_id=dict(), - ) - - mutually_exclusive = [ - ['delegation_set_id', 'vpc_id'], - ['delegation_set_id', 'vpc_region'], - ] - - module = AnsibleAWSModule( - argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True, - ) - - zone_in = module.params.get('zone').lower() - state = module.params.get('state').lower() - vpc_id = module.params.get('vpc_id') - vpc_region = module.params.get('vpc_region') - - if not zone_in.endswith('.'): - zone_in += "." - - private_zone = bool(vpc_id and vpc_region) - - client = module.client('route53') - - zones = find_zones(module, client, zone_in, private_zone) - if state == 'present': - changed, result = create(module, client, matching_zones=zones) - elif state == 'absent': - changed, result = delete(module, client, matching_zones=zones) - - if isinstance(result, dict): - module.exit_json(changed=changed, result=result, **result) - else: - module.exit_json(changed=changed, result=result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py b/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py deleted file mode 100644 index 1955892b9f..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/python -# (c) 2019, XLAB d.o.o <www.xlab.si> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: s3_bucket_notification -short_description: Creates, updates or deletes S3 Bucket notification for lambda -description: - - This module allows the management of AWS Lambda function bucket event mappings via the - Ansible framework. Use module M(lambda) to manage the lambda function itself, M(lambda_alias) - to manage function aliases and M(lambda_policy) to modify lambda permissions. -notes: - - This module heavily depends on M(lambda_policy) as you need to allow C(lambda:InvokeFunction) - permission for your lambda function. -version_added: "2.9" - -author: - - XLAB d.o.o. (@xlab-si) - - Aljaz Kosir (@aljazkosir) - - Miha Plesko (@miha-plesko) -options: - event_name: - description: - - Unique name for event notification on bucket. - required: true - type: str - lambda_function_arn: - description: - - The ARN of the lambda function. - aliases: ['function_arn'] - type: str - bucket_name: - description: - - S3 bucket name. - required: true - type: str - state: - description: - - Describes the desired state. - default: "present" - choices: ["present", "absent"] - type: str - lambda_alias: - description: - - Name of the Lambda function alias. - - Mutually exclusive with I(lambda_version). - type: str - lambda_version: - description: - - Version of the Lambda function. - - Mutually exclusive with I(lambda_alias). - type: int - events: - description: - - Events that you want to be triggering notifications. You can select multiple events to send - to the same destination, you can set up different events to send to different destinations, - and you can set up a prefix or suffix for an event. However, for each bucket, - individual events cannot have multiple configurations with overlapping prefixes or - suffixes that could match the same object key. - - Required when I(state=present). - choices: ['s3:ObjectCreated:*', 's3:ObjectCreated:Put', 's3:ObjectCreated:Post', - 's3:ObjectCreated:Copy', 's3:ObjectCreated:CompleteMultipartUpload', - 's3:ObjectRemoved:*', 's3:ObjectRemoved:Delete', - 's3:ObjectRemoved:DeleteMarkerCreated', 's3:ObjectRestore:Post', - 's3:ObjectRestore:Completed', 's3:ReducedRedundancyLostObject'] - type: list - elements: str - prefix: - description: - - Optional prefix to limit the notifications to objects with keys that start with matching - characters. - type: str - suffix: - description: - - Optional suffix to limit the notifications to objects with keys that end with matching - characters. - type: str -requirements: - - boto3 -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' ---- -# Example that creates a lambda event notification for a bucket -- hosts: localhost - gather_facts: no - tasks: - - name: Process jpg image - s3_bucket_notification: - state: present - event_name: on_file_add_or_remove - bucket_name: test-bucket - function_name: arn:aws:lambda:us-east-2:526810320200:function:test-lambda - events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] - prefix: images/ - suffix: .jpg -''' - -RETURN = ''' -notification_configuration: - description: list of currently applied notifications - returned: success - type: list -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # will be protected by AnsibleAWSModule - - -class AmazonBucket: - def __init__(self, client, bucket_name): - self.client = client - self.bucket_name = bucket_name - self._full_config_cache = None - - def full_config(self): - if self._full_config_cache is None: - self._full_config_cache = [Config.from_api(cfg) for cfg in - self.client.get_bucket_notification_configuration( - Bucket=self.bucket_name).get( - 'LambdaFunctionConfigurations', list())] - return self._full_config_cache - - def current_config(self, config_name): - for config in self.full_config(): - if config.raw['Id'] == config_name: - return config - - def apply_config(self, desired): - configs = [cfg.raw for cfg in self.full_config() if cfg.name != desired.raw['Id']] - configs.append(desired.raw) - self._upload_bucket_config(configs) - return configs - - def delete_config(self, desired): - configs = [cfg.raw for cfg in self.full_config() if cfg.name != desired.raw['Id']] - self._upload_bucket_config(configs) - return configs - - def _upload_bucket_config(self, config): - self.client.put_bucket_notification_configuration( - Bucket=self.bucket_name, - NotificationConfiguration={ - 'LambdaFunctionConfigurations': config - }) - - -class Config: - def __init__(self, content): - self._content = content - self.name = content['Id'] - - @property - def raw(self): - return self._content - - def __eq__(self, other): - if other: - return self.raw == other.raw - return False - - @classmethod - def from_params(cls, **params): - function_arn = params['lambda_function_arn'] - - qualifier = None - if params['lambda_version'] > 0: - qualifier = str(params['lambda_version']) - elif params['lambda_alias']: - qualifier = str(params['lambda_alias']) - if qualifier: - params['lambda_function_arn'] = '{0}:{1}'.format(function_arn, qualifier) - - return cls({ - 'Id': params['event_name'], - 'LambdaFunctionArn': params['lambda_function_arn'], - 'Events': sorted(params['events']), - 'Filter': { - 'Key': { - 'FilterRules': [{ - 'Name': 'Prefix', - 'Value': params['prefix'] - }, { - 'Name': 'Suffix', - 'Value': params['suffix'] - }] - } - } - }) - - @classmethod - def from_api(cls, config): - return cls(config) - - -def main(): - event_types = ['s3:ObjectCreated:*', 's3:ObjectCreated:Put', 's3:ObjectCreated:Post', - 's3:ObjectCreated:Copy', 's3:ObjectCreated:CompleteMultipartUpload', - 's3:ObjectRemoved:*', 's3:ObjectRemoved:Delete', - 's3:ObjectRemoved:DeleteMarkerCreated', 's3:ObjectRestore:Post', - 's3:ObjectRestore:Completed', 's3:ReducedRedundancyLostObject'] - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent']), - event_name=dict(required=True), - lambda_function_arn=dict(aliases=['function_arn']), - bucket_name=dict(required=True), - events=dict(type='list', default=[], choices=event_types), - prefix=dict(default=''), - suffix=dict(default=''), - lambda_alias=dict(), - lambda_version=dict(type='int', default=0), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[['lambda_alias', 'lambda_version']], - required_if=[['state', 'present', ['events']]] - ) - - bucket = AmazonBucket(module.client('s3'), module.params['bucket_name']) - current = bucket.current_config(module.params['event_name']) - desired = Config.from_params(**module.params) - notification_configuration = [cfg.raw for cfg in bucket.full_config()] - - state = module.params['state'] - try: - if (state == 'present' and current == desired) or (state == 'absent' and not current): - changed = False - elif module.check_mode: - changed = True - elif state == 'present': - changed = True - notification_configuration = bucket.apply_config(desired) - elif state == 'absent': - changed = True - notification_configuration = bucket.delete_config(desired) - except (ClientError, BotoCoreError) as e: - module.fail_json(msg='{0}'.format(e)) - - module.exit_json(**dict(changed=changed, - notification_configuration=[camel_dict_to_snake_dict(cfg) for cfg in - notification_configuration])) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_lifecycle.py b/lib/ansible/modules/cloud/amazon/s3_lifecycle.py deleted file mode 100644 index 73f89c95e9..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_lifecycle.py +++ /dev/null @@ -1,520 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: s3_lifecycle -short_description: Manage s3 bucket lifecycle rules in AWS -description: - - Manage s3 bucket lifecycle rules in AWS -version_added: "2.0" -author: "Rob White (@wimnat)" -notes: - - If specifying expiration time as days then transition time must also be specified in days - - If specifying expiration time as a date then transition time must also be specified as a date -requirements: - - python-dateutil -options: - name: - description: - - "Name of the s3 bucket" - required: true - type: str - expiration_date: - description: - - > - Indicates the lifetime of the objects that are subject to the rule by the date they will expire. The value must be ISO-8601 format, the time must - be midnight and a GMT timezone must be specified. - type: str - expiration_days: - description: - - "Indicates the lifetime, in days, of the objects that are subject to the rule. The value must be a non-zero positive integer." - type: int - prefix: - description: - - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." - type: str - purge_transitions: - description: - - > - "Whether to replace all the current transition(s) with the new transition(s). When false, the provided transition(s) - will be added, replacing transitions with the same storage_class. When true, existing transitions will be removed and - replaced with the new transition(s) - default: true - type: bool - version_added: 2.6 - noncurrent_version_expiration_days: - description: - - 'Delete noncurrent versions this many days after they become noncurrent' - required: false - version_added: 2.6 - type: int - noncurrent_version_storage_class: - description: - - 'Transition noncurrent versions to this storage class' - default: glacier - choices: ['glacier', 'onezone_ia', 'standard_ia'] - required: false - version_added: 2.6 - type: str - noncurrent_version_transition_days: - description: - - 'Transition noncurrent versions this many days after they become noncurrent' - required: false - version_added: 2.6 - type: int - noncurrent_version_transitions: - description: - - > - A list of transition behaviors to be applied to noncurrent versions for the rule. Each storage class may be used only once. Each transition - behavior contains these elements - I(transition_days) - I(storage_class) - version_added: 2.6 - type: list - rule_id: - description: - - "Unique identifier for the rule. The value cannot be longer than 255 characters. A unique value for the rule will be generated if no value is provided." - type: str - state: - description: - - "Create or remove the lifecycle rule" - default: present - choices: [ 'present', 'absent' ] - type: str - status: - description: - - "If 'enabled', the rule is currently being applied. If 'disabled', the rule is not currently being applied." - default: enabled - choices: [ 'enabled', 'disabled' ] - type: str - storage_class: - description: - - "The storage class to transition to. Currently there are two supported values - 'glacier', 'onezone_ia', or 'standard_ia'." - - "The 'standard_ia' class is only being available from Ansible version 2.2." - default: glacier - choices: [ 'glacier', 'onezone_ia', 'standard_ia'] - type: str - transition_date: - description: - - > - Indicates the lifetime of the objects that are subject to the rule by the date they will transition to a different storage class. - The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified. If transition_days is not specified, - this parameter is required." - type: str - transition_days: - description: - - "Indicates when, in days, an object transitions to a different storage class. If transition_date is not specified, this parameter is required." - type: int - transitions: - description: - - A list of transition behaviors to be applied to the rule. Each storage class may be used only once. Each transition - behavior may contain these elements - I(transition_days) - I(transition_date) - I(storage_class) - version_added: 2.6 - type: list - requester_pays: - description: - - The I(requester_pays) option does nothing and will be removed in Ansible 2.14. - type: bool -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Configure a lifecycle rule on a bucket to expire (delete) items with a prefix of /logs/ after 30 days -- s3_lifecycle: - name: mybucket - expiration_days: 30 - prefix: logs/ - status: enabled - state: present - -# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier after 7 days and then delete after 90 days -- s3_lifecycle: - name: mybucket - transition_days: 7 - expiration_days: 90 - prefix: logs/ - status: enabled - state: present - -# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier on 31 Dec 2020 and then delete on 31 Dec 2030. -# Note that midnight GMT must be specified. -# Be sure to quote your date strings -- s3_lifecycle: - name: mybucket - transition_date: "2020-12-30T00:00:00.000Z" - expiration_date: "2030-12-30T00:00:00.000Z" - prefix: logs/ - status: enabled - state: present - -# Disable the rule created above -- s3_lifecycle: - name: mybucket - prefix: logs/ - status: disabled - state: present - -# Delete the lifecycle rule created above -- s3_lifecycle: - name: mybucket - prefix: logs/ - state: absent - -# Configure a lifecycle rule to transition all backup files older than 31 days in /backups/ to standard infrequent access class. -- s3_lifecycle: - name: mybucket - prefix: backups/ - storage_class: standard_ia - transition_days: 31 - state: present - status: enabled - -# Configure a lifecycle rule to transition files to infrequent access after 30 days and glacier after 90 -- s3_lifecycle: - name: mybucket - prefix: logs/ - state: present - status: enabled - transitions: - - transition_days: 30 - storage_class: standard_ia - - transition_days: 90 - storage_class: glacier -''' - -from copy import deepcopy -import datetime - -try: - import dateutil.parser - HAS_DATEUTIL = True -except ImportError: - HAS_DATEUTIL = False - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # handled by AnsibleAwsModule - -from ansible.module_utils.aws.core import AnsibleAWSModule - - -def create_lifecycle_rule(client, module): - - name = module.params.get("name") - expiration_date = module.params.get("expiration_date") - expiration_days = module.params.get("expiration_days") - noncurrent_version_expiration_days = module.params.get("noncurrent_version_expiration_days") - noncurrent_version_transition_days = module.params.get("noncurrent_version_transition_days") - noncurrent_version_transitions = module.params.get("noncurrent_version_transitions") - noncurrent_version_storage_class = module.params.get("noncurrent_version_storage_class") - prefix = module.params.get("prefix") or "" - rule_id = module.params.get("rule_id") - status = module.params.get("status") - storage_class = module.params.get("storage_class") - transition_date = module.params.get("transition_date") - transition_days = module.params.get("transition_days") - transitions = module.params.get("transitions") - purge_transitions = module.params.get("purge_transitions") - changed = False - - # Get the bucket's current lifecycle rules - try: - current_lifecycle = client.get_bucket_lifecycle_configuration(Bucket=name) - current_lifecycle_rules = current_lifecycle['Rules'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchLifecycleConfiguration': - current_lifecycle_rules = [] - else: - module.fail_json_aws(e) - except BotoCoreError as e: - module.fail_json_aws(e) - - rule = dict(Filter=dict(Prefix=prefix), Status=status.title()) - if rule_id is not None: - rule['ID'] = rule_id - # Create expiration - if expiration_days is not None: - rule['Expiration'] = dict(Days=expiration_days) - elif expiration_date is not None: - rule['Expiration'] = dict(Date=expiration_date) - - if noncurrent_version_expiration_days is not None: - rule['NoncurrentVersionExpiration'] = dict(NoncurrentDays=noncurrent_version_expiration_days) - - if transition_days is not None: - rule['Transitions'] = [dict(Days=transition_days, StorageClass=storage_class.upper()), ] - - elif transition_date is not None: - rule['Transitions'] = [dict(Date=transition_date, StorageClass=storage_class.upper()), ] - - if transitions is not None: - if not rule.get('Transitions'): - rule['Transitions'] = [] - for transition in transitions: - t_out = dict() - if transition.get('transition_date'): - t_out['Date'] = transition['transition_date'] - elif transition.get('transition_days'): - t_out['Days'] = transition['transition_days'] - if transition.get('storage_class'): - t_out['StorageClass'] = transition['storage_class'].upper() - rule['Transitions'].append(t_out) - - if noncurrent_version_transition_days is not None: - rule['NoncurrentVersionTransitions'] = [dict(NoncurrentDays=noncurrent_version_transition_days, - StorageClass=noncurrent_version_storage_class.upper()), ] - - if noncurrent_version_transitions is not None: - if not rule.get('NoncurrentVersionTransitions'): - rule['NoncurrentVersionTransitions'] = [] - for noncurrent_version_transition in noncurrent_version_transitions: - t_out = dict() - t_out['NoncurrentDays'] = noncurrent_version_transition['transition_days'] - if noncurrent_version_transition.get('storage_class'): - t_out['StorageClass'] = noncurrent_version_transition['storage_class'].upper() - rule['NoncurrentVersionTransitions'].append(t_out) - - lifecycle_configuration = dict(Rules=[]) - appended = False - # If current_lifecycle_obj is not None then we have rules to compare, otherwise just add the rule - if current_lifecycle_rules: - # If rule ID exists, use that for comparison otherwise compare based on prefix - for existing_rule in current_lifecycle_rules: - if rule.get('ID') == existing_rule.get('ID') and rule['Filter']['Prefix'] != existing_rule.get('Filter', {}).get('Prefix', ''): - existing_rule.pop('ID') - elif rule_id is None and rule['Filter']['Prefix'] == existing_rule.get('Filter', {}).get('Prefix', ''): - existing_rule.pop('ID') - if rule.get('ID') == existing_rule.get('ID'): - changed_, appended_ = update_or_append_rule(rule, existing_rule, purge_transitions, lifecycle_configuration) - changed = changed_ or changed - appended = appended_ or appended - else: - lifecycle_configuration['Rules'].append(existing_rule) - - # If nothing appended then append now as the rule must not exist - if not appended: - lifecycle_configuration['Rules'].append(rule) - changed = True - else: - lifecycle_configuration['Rules'].append(rule) - changed = True - - # Write lifecycle to bucket - try: - client.put_bucket_lifecycle_configuration(Bucket=name, LifecycleConfiguration=lifecycle_configuration) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - module.exit_json(changed=changed) - - -def update_or_append_rule(new_rule, existing_rule, purge_transitions, lifecycle_obj): - changed = False - if existing_rule['Status'] != new_rule['Status']: - if not new_rule.get('Transitions') and existing_rule.get('Transitions'): - new_rule['Transitions'] = existing_rule['Transitions'] - if not new_rule.get('Expiration') and existing_rule.get('Expiration'): - new_rule['Expiration'] = existing_rule['Expiration'] - if not new_rule.get('NoncurrentVersionExpiration') and existing_rule.get('NoncurrentVersionExpiration'): - new_rule['NoncurrentVersionExpiration'] = existing_rule['NoncurrentVersionExpiration'] - lifecycle_obj['Rules'].append(new_rule) - changed = True - appended = True - else: - if not purge_transitions: - merge_transitions(new_rule, existing_rule) - if compare_rule(new_rule, existing_rule, purge_transitions): - lifecycle_obj['Rules'].append(new_rule) - appended = True - else: - lifecycle_obj['Rules'].append(new_rule) - changed = True - appended = True - return changed, appended - - -def compare_rule(rule_a, rule_b, purge_transitions): - - # Copy objects - rule1 = deepcopy(rule_a) - rule2 = deepcopy(rule_b) - - if purge_transitions: - return rule1 == rule2 - else: - transitions1 = rule1.pop('Transitions', []) - transitions2 = rule2.pop('Transitions', []) - noncurrent_transtions1 = rule1.pop('NoncurrentVersionTransitions', []) - noncurrent_transtions2 = rule2.pop('NoncurrentVersionTransitions', []) - if rule1 != rule2: - return False - for transition in transitions1: - if transition not in transitions2: - return False - for transition in noncurrent_transtions1: - if transition not in noncurrent_transtions2: - return False - return True - - -def merge_transitions(updated_rule, updating_rule): - # because of the legal s3 transitions, we know only one can exist for each storage class. - # So, our strategy is build some dicts, keyed on storage class and add the storage class transitions that are only - # in updating_rule to updated_rule - updated_transitions = {} - updating_transitions = {} - for transition in updated_rule.get('Transitions', []): - updated_transitions[transition['StorageClass']] = transition - for transition in updating_rule.get('Transitions', []): - updating_transitions[transition['StorageClass']] = transition - for storage_class, transition in updating_transitions.items(): - if updated_transitions.get(storage_class) is None: - updated_rule['Transitions'].append(transition) - - -def destroy_lifecycle_rule(client, module): - - name = module.params.get("name") - prefix = module.params.get("prefix") - rule_id = module.params.get("rule_id") - changed = False - - if prefix is None: - prefix = "" - - # Get the bucket's current lifecycle rules - try: - current_lifecycle_rules = client.get_bucket_lifecycle_configuration(Bucket=name)['Rules'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchLifecycleConfiguration': - current_lifecycle_rules = [] - else: - module.fail_json_aws(e) - except BotoCoreError as e: - module.fail_json_aws(e) - - # Create lifecycle - lifecycle_obj = dict(Rules=[]) - - # Check if rule exists - # If an ID exists, use that otherwise compare based on prefix - if rule_id is not None: - for existing_rule in current_lifecycle_rules: - if rule_id == existing_rule['ID']: - # We're not keeping the rule (i.e. deleting) so mark as changed - changed = True - else: - lifecycle_obj['Rules'].append(existing_rule) - else: - for existing_rule in current_lifecycle_rules: - if prefix == existing_rule['Filter']['Prefix']: - # We're not keeping the rule (i.e. deleting) so mark as changed - changed = True - else: - lifecycle_obj['Rules'].append(existing_rule) - - # Write lifecycle to bucket or, if there no rules left, delete lifecycle configuration - try: - if lifecycle_obj['Rules']: - client.put_bucket_lifecycle_configuration(Bucket=name, LifecycleConfiguration=lifecycle_obj) - elif current_lifecycle_rules: - changed = True - client.delete_bucket_lifecycle(Bucket=name) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e) - module.exit_json(changed=changed) - - -def main(): - argument_spec = dict( - name=dict(required=True, type='str'), - expiration_days=dict(type='int'), - expiration_date=dict(), - noncurrent_version_expiration_days=dict(type='int'), - noncurrent_version_storage_class=dict(default='glacier', type='str', choices=['glacier', 'onezone_ia', 'standard_ia']), - noncurrent_version_transition_days=dict(type='int'), - noncurrent_version_transitions=dict(type='list'), - prefix=dict(), - requester_pays=dict(type='bool', removed_in_version='2.14'), - rule_id=dict(), - state=dict(default='present', choices=['present', 'absent']), - status=dict(default='enabled', choices=['enabled', 'disabled']), - storage_class=dict(default='glacier', type='str', choices=['glacier', 'onezone_ia', 'standard_ia']), - transition_days=dict(type='int'), - transition_date=dict(), - transitions=dict(type='list'), - purge_transitions=dict(default='yes', type='bool') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['expiration_days', 'expiration_date'], - ['expiration_days', 'transition_date'], - ['transition_days', 'transition_date'], - ['transition_days', 'expiration_date'], - ['transition_days', 'transitions'], - ['transition_date', 'transitions'], - ['noncurrent_version_transition_days', 'noncurrent_version_transitions'], - ],) - - if not HAS_DATEUTIL: - module.fail_json(msg='dateutil required for this module') - - client = module.client('s3') - - expiration_date = module.params.get("expiration_date") - transition_date = module.params.get("transition_date") - state = module.params.get("state") - - if state == 'present' and module.params["status"] == "enabled": # allow deleting/disabling a rule by id/prefix - - required_when_present = ('expiration_date', 'expiration_days', 'transition_date', - 'transition_days', 'transitions', 'noncurrent_version_expiration_days', - 'noncurrent_version_transition_days', - 'noncurrent_version_transitions') - for param in required_when_present: - if module.params.get(param): - break - else: - msg = "one of the following is required when 'state' is 'present': %s" % ', '.join(required_when_present) - module.fail_json(msg=msg) - # If expiration_date set, check string is valid - if expiration_date is not None: - try: - datetime.datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%S.000Z") - except ValueError as e: - module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") - - if transition_date is not None: - try: - datetime.datetime.strptime(transition_date, "%Y-%m-%dT%H:%M:%S.000Z") - except ValueError as e: - module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") - - if state == 'present': - create_lifecycle_rule(client, module) - elif state == 'absent': - destroy_lifecycle_rule(client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_logging.py b/lib/ansible/modules/cloud/amazon/s3_logging.py deleted file mode 100644 index d58a58f40c..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_logging.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: s3_logging -short_description: Manage logging facility of an s3 bucket in AWS -description: - - Manage logging facility of an s3 bucket in AWS -version_added: "2.0" -author: Rob White (@wimnat) -options: - name: - description: - - "Name of the s3 bucket." - required: true - type: str - state: - description: - - "Enable or disable logging." - default: present - choices: [ 'present', 'absent' ] - type: str - target_bucket: - description: - - "The bucket to log to. Required when state=present." - type: str - target_prefix: - description: - - "The prefix that should be prepended to the generated log files written to the target_bucket." - default: "" - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Enable logging of s3 bucket mywebsite.com to s3 bucket mylogs - s3_logging: - name: mywebsite.com - target_bucket: mylogs - target_prefix: logs/mywebsite.com - state: present - -- name: Remove logging on an s3 bucket - s3_logging: - name: mywebsite.com - state: absent - -''' - -try: - import boto.ec2 - from boto.s3.connection import OrdinaryCallingFormat, Location - from boto.exception import S3ResponseError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import AnsibleAWSError, ec2_argument_spec, get_aws_connection_info - - -def compare_bucket_logging(bucket, target_bucket, target_prefix): - - bucket_log_obj = bucket.get_logging_status() - if bucket_log_obj.target != target_bucket or bucket_log_obj.prefix != target_prefix: - return False - else: - return True - - -def enable_bucket_logging(connection, module): - - bucket_name = module.params.get("name") - target_bucket = module.params.get("target_bucket") - target_prefix = module.params.get("target_prefix") - changed = False - - try: - bucket = connection.get_bucket(bucket_name) - except S3ResponseError as e: - module.fail_json(msg=e.message) - - try: - if not compare_bucket_logging(bucket, target_bucket, target_prefix): - # Before we can enable logging we must give the log-delivery group WRITE and READ_ACP permissions to the target bucket - try: - target_bucket_obj = connection.get_bucket(target_bucket) - except S3ResponseError as e: - if e.status == 301: - module.fail_json(msg="the logging target bucket must be in the same region as the bucket being logged") - else: - module.fail_json(msg=e.message) - target_bucket_obj.set_as_logging_target() - - bucket.enable_logging(target_bucket, target_prefix) - changed = True - - except S3ResponseError as e: - module.fail_json(msg=e.message) - - module.exit_json(changed=changed) - - -def disable_bucket_logging(connection, module): - - bucket_name = module.params.get("name") - changed = False - - try: - bucket = connection.get_bucket(bucket_name) - if not compare_bucket_logging(bucket, None, None): - bucket.disable_logging() - changed = True - except S3ResponseError as e: - module.fail_json(msg=e.message) - - module.exit_json(changed=changed) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True), - target_bucket=dict(required=False, default=None), - target_prefix=dict(required=False, default=""), - state=dict(required=False, default='present', choices=['present', 'absent']) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region in ('us-east-1', '', None): - # S3ism for the US Standard region - location = Location.DEFAULT - else: - # Boto uses symbolic names for locations but region strings will - # actually work fine for everything except us-east-1 (US Standard) - location = region - try: - connection = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_params) - # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases - if connection is None: - connection = boto.connect_s3(**aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - - state = module.params.get("state") - - if state == 'present': - enable_bucket_logging(connection, module) - elif state == 'absent': - disable_bucket_logging(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_sync.py b/lib/ansible/modules/cloud/amazon/s3_sync.py deleted file mode 100644 index faf2617397..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_sync.py +++ /dev/null @@ -1,567 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: s3_sync -short_description: Efficiently upload multiple files to S3 -description: - - The S3 module is great, but it is very slow for a large volume of files- even a dozen will be noticeable. In addition to speed, it handles globbing, - inclusions/exclusions, mime types, expiration mapping, recursion, cache control and smart directory mapping. -version_added: "2.3" -options: - mode: - description: - - sync direction. - default: 'push' - choices: [ 'push' ] - type: str - file_change_strategy: - description: - - Difference determination method to allow changes-only syncing. Unlike rsync, files are not patched- they are fully skipped or fully uploaded. - - date_size will upload if file sizes don't match or if local file modified date is newer than s3's version - - checksum will compare etag values based on s3's implementation of chunked md5s. - - force will always upload all files. - required: false - default: 'date_size' - choices: [ 'force', 'checksum', 'date_size' ] - type: str - bucket: - description: - - Bucket name. - required: true - type: str - key_prefix: - description: - - In addition to file path, prepend s3 path with this prefix. Module will add slash at end of prefix if necessary. - required: false - type: str - file_root: - description: - - File/directory path for synchronization. This is a local path. - - This root path is scrubbed from the key name, so subdirectories will remain as keys. - required: true - type: path - permission: - description: - - Canned ACL to apply to synced files. - - Changing this ACL only changes newly synced files, it does not trigger a full reupload. - required: false - choices: - - 'private' - - 'public-read' - - 'public-read-write' - - 'authenticated-read' - - 'aws-exec-read' - - 'bucket-owner-read' - - 'bucket-owner-full-control' - type: str - mime_map: - description: - - > - Dict entry from extension to MIME type. This will override any default/sniffed MIME type. - For example C({".txt": "application/text", ".yml": "application/text"}) - required: false - type: dict - include: - description: - - Shell pattern-style file matching. - - Used before exclude to determine eligible files (for instance, only "*.gif") - - For multiple patterns, comma-separate them. - required: false - default: "*" - type: str - exclude: - description: - - Shell pattern-style file matching. - - Used after include to remove files (for instance, skip "*.txt") - - For multiple patterns, comma-separate them. - required: false - default: ".*" - type: str - cache_control: - description: - - Cache-Control header set on uploaded objects. - - Directives are separated by commas. - required: false - version_added: "2.4" - type: str - delete: - description: - - Remove remote files that exist in bucket but are not present in the file root. - required: false - default: no - version_added: "2.4" - type: bool - retries: - description: - - The I(retries) option does nothing and will be removed in Ansible 2.14. - type: str - -requirements: - - boto3 >= 1.4.4 - - botocore - - python-dateutil - -author: Ted Timmons (@tedder) -extends_documentation_fragment: -- aws -- ec2 -''' - -EXAMPLES = ''' -- name: basic upload - s3_sync: - bucket: tedder - file_root: roles/s3/files/ - -- name: all the options - s3_sync: - bucket: tedder - file_root: roles/s3/files - mime_map: - .yml: application/text - .json: application/text - key_prefix: config_files/web - file_change_strategy: force - permission: public-read - cache_control: "public, max-age=31536000" - include: "*" - exclude: "*.txt,.*" -''' - -RETURN = ''' -filelist_initial: - description: file listing (dicts) from initial globbing - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "modified_epoch": 1477416706 - }] -filelist_local_etag: - description: file listing (dicts) including calculated local etag - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "mime_type": "application/json", - "modified_epoch": 1477416706, - "s3_path": "s3sync/policy.json" - }] -filelist_s3: - description: file listing (dicts) including information about previously-uploaded versions - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "mime_type": "application/json", - "modified_epoch": 1477416706, - "s3_path": "s3sync/policy.json" - }] -filelist_typed: - description: file listing (dicts) with calculated or overridden mime types - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "mime_type": "application/json", - "modified_epoch": 1477416706 - }] -filelist_actionable: - description: file listing (dicts) of files that will be uploaded after the strategy decision - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "mime_type": "application/json", - "modified_epoch": 1477931256, - "s3_path": "s3sync/policy.json", - "whysize": "151 / 151", - "whytime": "1477931256 / 1477929260" - }] -uploaded: - description: file listing (dicts) of files that were actually uploaded - returned: always - type: list - sample: [{ - "bytes": 151, - "chopped_path": "policy.json", - "fullpath": "roles/cf/files/policy.json", - "s3_path": "s3sync/policy.json", - "whysize": "151 / 151", - "whytime": "1477931637 / 1477931489" - }] - -''' - -import datetime -import fnmatch -import hashlib -import mimetypes -import os -import stat as osstat # os.stat constants -import traceback - -# import module snippets -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ec2_argument_spec, boto3_conn, get_aws_connection_info, HAS_BOTO3, boto_exception -from ansible.module_utils._text import to_text - -try: - from dateutil import tz - HAS_DATEUTIL = True -except ImportError: - HAS_DATEUTIL = False - -try: - import botocore -except ImportError: - # Handled by imported HAS_BOTO3 - pass - - -# the following function, calculate_multipart_etag, is from tlastowka -# on github and is used under its (compatible) GPL license. So this -# license applies to the following function. -# source: https://github.com/tlastowka/calculate_multipart_etag/blob/master/calculate_multipart_etag.py -# -# calculate_multipart_etag Copyright (C) 2015 -# Tony Lastowka <tlastowka at gmail dot com> -# https://github.com/tlastowka -# -# -# calculate_multipart_etag is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# calculate_multipart_etag is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with calculate_multipart_etag. If not, see <http://www.gnu.org/licenses/>. - -DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024 - - -def calculate_multipart_etag(source_path, chunk_size=DEFAULT_CHUNK_SIZE): - """ - calculates a multipart upload etag for amazon s3 - - Arguments: - - source_path -- The file to calculate the etag for - chunk_size -- The chunk size to calculate for. - """ - - md5s = [] - - with open(source_path, 'rb') as fp: - while True: - - data = fp.read(chunk_size) - - if not data: - break - md5s.append(hashlib.md5(data)) - - if len(md5s) == 1: - new_etag = '"{0}"'.format(md5s[0].hexdigest()) - else: # > 1 - digests = b"".join(m.digest() for m in md5s) - - new_md5 = hashlib.md5(digests) - new_etag = '"{0}-{1}"'.format(new_md5.hexdigest(), len(md5s)) - - return new_etag - - -def gather_files(fileroot, include=None, exclude=None): - ret = [] - for (dirpath, dirnames, filenames) in os.walk(fileroot): - for fn in filenames: - fullpath = os.path.join(dirpath, fn) - # include/exclude - if include: - found = False - for x in include.split(','): - if fnmatch.fnmatch(fn, x): - found = True - if not found: - # not on the include list, so we don't want it. - continue - - if exclude: - found = False - for x in exclude.split(','): - if fnmatch.fnmatch(fn, x): - found = True - if found: - # skip it, even if previously included. - continue - - chopped_path = os.path.relpath(fullpath, start=fileroot) - fstat = os.stat(fullpath) - f_size = fstat[osstat.ST_SIZE] - f_modified_epoch = fstat[osstat.ST_MTIME] - ret.append({ - 'fullpath': fullpath, - 'chopped_path': chopped_path, - 'modified_epoch': f_modified_epoch, - 'bytes': f_size, - }) - # dirpath = path *to* the directory - # dirnames = subdirs *in* our directory - # filenames - return ret - - -def calculate_s3_path(filelist, key_prefix=''): - ret = [] - for fileentry in filelist: - # don't modify the input dict - retentry = fileentry.copy() - retentry['s3_path'] = os.path.join(key_prefix, fileentry['chopped_path']) - ret.append(retentry) - return ret - - -def calculate_local_etag(filelist, key_prefix=''): - '''Really, "calculate md5", but since AWS uses their own format, we'll just call - it a "local etag". TODO optimization: only calculate if remote key exists.''' - ret = [] - for fileentry in filelist: - # don't modify the input dict - retentry = fileentry.copy() - retentry['local_etag'] = calculate_multipart_etag(fileentry['fullpath']) - ret.append(retentry) - return ret - - -def determine_mimetypes(filelist, override_map): - ret = [] - for fileentry in filelist: - retentry = fileentry.copy() - localfile = fileentry['fullpath'] - - # reminder: file extension is '.txt', not 'txt'. - file_extension = os.path.splitext(localfile)[1] - if override_map and override_map.get(file_extension): - # override? use it. - retentry['mime_type'] = override_map[file_extension] - else: - # else sniff it - retentry['mime_type'], retentry['encoding'] = mimetypes.guess_type(localfile, strict=False) - - # might be None or '' from one of the above. Not a great type but better than nothing. - if not retentry['mime_type']: - retentry['mime_type'] = 'application/octet-stream' - - ret.append(retentry) - - return ret - - -def head_s3(s3, bucket, s3keys): - retkeys = [] - for entry in s3keys: - retentry = entry.copy() - # don't modify the input dict - try: - retentry['s3_head'] = s3.head_object(Bucket=bucket, Key=entry['s3_path']) - except botocore.exceptions.ClientError as err: - if (hasattr(err, 'response') and - 'ResponseMetadata' in err.response and - 'HTTPStatusCode' in err.response['ResponseMetadata'] and - str(err.response['ResponseMetadata']['HTTPStatusCode']) == '404'): - pass - else: - raise Exception(err) - # error_msg = boto_exception(err) - # return {'error': error_msg} - retkeys.append(retentry) - return retkeys - - -def filter_list(s3, bucket, s3filelist, strategy): - keeplist = list(s3filelist) - - for e in keeplist: - e['_strategy'] = strategy - - # init/fetch info from S3 if we're going to use it for comparisons - if not strategy == 'force': - keeplist = head_s3(s3, bucket, s3filelist) - - # now actually run the strategies - if strategy == 'checksum': - for entry in keeplist: - if entry.get('s3_head'): - # since we have a remote s3 object, compare the values. - if entry['s3_head']['ETag'] == entry['local_etag']: - # files match, so remove the entry - entry['skip_flag'] = True - else: - # file etags don't match, keep the entry. - pass - else: # we don't have an etag, so we'll keep it. - pass - elif strategy == 'date_size': - for entry in keeplist: - if entry.get('s3_head'): - # fstat = entry['stat'] - local_modified_epoch = entry['modified_epoch'] - local_size = entry['bytes'] - - # py2's datetime doesn't have a timestamp() field, so we have to revert to something more awkward. - # remote_modified_epoch = entry['s3_head']['LastModified'].timestamp() - remote_modified_datetime = entry['s3_head']['LastModified'] - delta = (remote_modified_datetime - datetime.datetime(1970, 1, 1, tzinfo=tz.tzutc())) - remote_modified_epoch = delta.seconds + (delta.days * 86400) - - remote_size = entry['s3_head']['ContentLength'] - - entry['whytime'] = '{0} / {1}'.format(local_modified_epoch, remote_modified_epoch) - entry['whysize'] = '{0} / {1}'.format(local_size, remote_size) - - if local_modified_epoch <= remote_modified_epoch and local_size == remote_size: - entry['skip_flag'] = True - else: - entry['why'] = "no s3_head" - # else: probably 'force'. Basically we don't skip with any with other strategies. - else: - pass - - # prune 'please skip' entries, if any. - return [x for x in keeplist if not x.get('skip_flag')] - - -def upload_files(s3, bucket, filelist, params): - ret = [] - for entry in filelist: - args = { - 'ContentType': entry['mime_type'] - } - if params.get('permission'): - args['ACL'] = params['permission'] - if params.get('cache_control'): - args['CacheControl'] = params['cache_control'] - # if this fails exception is caught in main() - s3.upload_file(entry['fullpath'], bucket, entry['s3_path'], ExtraArgs=args, Callback=None, Config=None) - ret.append(entry) - return ret - - -def remove_files(s3, sourcelist, params): - bucket = params.get('bucket') - key_prefix = params.get('key_prefix') - paginator = s3.get_paginator('list_objects_v2') - current_keys = set(x['Key'] for x in paginator.paginate(Bucket=bucket, Prefix=key_prefix).build_full_result().get('Contents', [])) - keep_keys = set(to_text(source_file['s3_path']) for source_file in sourcelist) - delete_keys = list(current_keys - keep_keys) - - # can delete 1000 objects at a time - groups_of_keys = [delete_keys[i:i + 1000] for i in range(0, len(delete_keys), 1000)] - for keys in groups_of_keys: - s3.delete_objects(Bucket=bucket, Delete={'Objects': [{'Key': key} for key in keys]}) - - return delete_keys - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - mode=dict(choices=['push'], default='push'), - file_change_strategy=dict(choices=['force', 'date_size', 'checksum'], default='date_size'), - bucket=dict(required=True), - key_prefix=dict(required=False, default=''), - file_root=dict(required=True, type='path'), - permission=dict(required=False, choices=['private', 'public-read', 'public-read-write', 'authenticated-read', - 'aws-exec-read', 'bucket-owner-read', 'bucket-owner-full-control']), - retries=dict(required=False, removed_in_version='2.14'), - mime_map=dict(required=False, type='dict'), - exclude=dict(required=False, default=".*"), - include=dict(required=False, default="*"), - cache_control=dict(required=False, default=''), - delete=dict(required=False, type='bool', default=False), - # future options: encoding, metadata, storage_class, retries - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - ) - - if not HAS_DATEUTIL: - module.fail_json(msg='dateutil required for this module') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - result = {} - mode = module.params['mode'] - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified") - s3 = boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, **aws_connect_kwargs) - - if mode == 'push': - try: - result['filelist_initial'] = gather_files(module.params['file_root'], exclude=module.params['exclude'], include=module.params['include']) - result['filelist_typed'] = determine_mimetypes(result['filelist_initial'], module.params.get('mime_map')) - result['filelist_s3'] = calculate_s3_path(result['filelist_typed'], module.params['key_prefix']) - result['filelist_local_etag'] = calculate_local_etag(result['filelist_s3']) - result['filelist_actionable'] = filter_list(s3, module.params['bucket'], result['filelist_local_etag'], module.params['file_change_strategy']) - result['uploads'] = upload_files(s3, module.params['bucket'], result['filelist_actionable'], module.params) - - if module.params['delete']: - result['removed'] = remove_files(s3, result['filelist_local_etag'], module.params) - - # mark changed if we actually upload something. - if result.get('uploads') or result.get('removed'): - result['changed'] = True - # result.update(filelist=actionable_filelist) - except botocore.exceptions.ClientError as err: - error_msg = boto_exception(err) - module.fail_json(msg=error_msg, exception=traceback.format_exc(), **camel_dict_to_snake_dict(err.response)) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_website.py b/lib/ansible/modules/cloud/amazon/s3_website.py deleted file mode 100644 index f8e76b0475..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_website.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: s3_website -short_description: Configure an s3 bucket as a website -description: - - Configure an s3 bucket as a website -version_added: "2.2" -requirements: [ boto3 ] -author: Rob White (@wimnat) -options: - name: - description: - - "Name of the s3 bucket" - required: true - type: str - error_key: - description: - - "The object key name to use when a 4XX class error occurs. To remove an error key, set to None." - type: str - redirect_all_requests: - description: - - "Describes the redirect behavior for every request to this s3 bucket website endpoint" - type: str - state: - description: - - "Add or remove s3 website configuration" - choices: [ 'present', 'absent' ] - required: true - type: str - suffix: - description: - - > - Suffix that is appended to a request that is for a directory on the website endpoint (e.g. if the suffix is index.html and you make a request to - samplebucket/images/ the data that is returned will be for the object with the key name images/index.html). The suffix must not include a slash - character. - default: index.html - type: str - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Configure an s3 bucket to redirect all requests to example.com -- s3_website: - name: mybucket.com - redirect_all_requests: example.com - state: present - -# Remove website configuration from an s3 bucket -- s3_website: - name: mybucket.com - state: absent - -# Configure an s3 bucket as a website with index and error pages -- s3_website: - name: mybucket.com - suffix: home.htm - error_key: errors/404.htm - state: present - -''' - -RETURN = ''' -index_document: - description: index document - type: complex - returned: always - contains: - suffix: - description: suffix that is appended to a request that is for a directory on the website endpoint - returned: success - type: str - sample: index.html -error_document: - description: error document - type: complex - returned: always - contains: - key: - description: object key name to use when a 4XX class error occurs - returned: when error_document parameter set - type: str - sample: error.html -redirect_all_requests_to: - description: where to redirect requests - type: complex - returned: always - contains: - host_name: - description: name of the host where requests will be redirected. - returned: when redirect all requests parameter set - type: str - sample: ansible.com - protocol: - description: protocol to use when redirecting requests. - returned: when redirect all requests parameter set - type: str - sample: https -routing_rules: - description: routing rules - type: list - returned: always - contains: - condition: - type: complex - description: A container for describing a condition that must be met for the specified redirect to apply. - contains: - http_error_code_returned_equals: - description: The HTTP error code when the redirect is applied. - returned: always - type: str - key_prefix_equals: - description: object key name prefix when the redirect is applied. For example, to redirect - requests for ExamplePage.html, the key prefix will be ExamplePage.html - returned: when routing rule present - type: str - sample: docs/ - redirect: - type: complex - description: Container for redirect information. - returned: always - contains: - host_name: - description: name of the host where requests will be redirected. - returned: when host name set as part of redirect rule - type: str - sample: ansible.com - http_redirect_code: - description: The HTTP redirect code to use on the response. - returned: when routing rule present - type: str - protocol: - description: Protocol to use when redirecting requests. - returned: when routing rule present - type: str - sample: http - replace_key_prefix_with: - description: object key prefix to use in the redirect request - returned: when routing rule present - type: str - sample: documents/ - replace_key_with: - description: object key prefix to use in the redirect request - returned: when routing rule present - type: str - sample: documents/ -''' - -import time - -try: - import boto3 - from botocore.exceptions import ClientError, ParamValidationError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO3, boto3_conn, camel_dict_to_snake_dict, ec2_argument_spec, - get_aws_connection_info) - - -def _create_redirect_dict(url): - - redirect_dict = {} - url_split = url.split(':') - - # Did we split anything? - if len(url_split) == 2: - redirect_dict[u'Protocol'] = url_split[0] - redirect_dict[u'HostName'] = url_split[1].replace('//', '') - elif len(url_split) == 1: - redirect_dict[u'HostName'] = url_split[0] - else: - raise ValueError('Redirect URL appears invalid') - - return redirect_dict - - -def _create_website_configuration(suffix, error_key, redirect_all_requests): - - website_configuration = {} - - if error_key is not None: - website_configuration['ErrorDocument'] = {'Key': error_key} - - if suffix is not None: - website_configuration['IndexDocument'] = {'Suffix': suffix} - - if redirect_all_requests is not None: - website_configuration['RedirectAllRequestsTo'] = _create_redirect_dict(redirect_all_requests) - - return website_configuration - - -def enable_or_update_bucket_as_website(client_connection, resource_connection, module): - - bucket_name = module.params.get("name") - redirect_all_requests = module.params.get("redirect_all_requests") - # If redirect_all_requests is set then don't use the default suffix that has been set - if redirect_all_requests is not None: - suffix = None - else: - suffix = module.params.get("suffix") - error_key = module.params.get("error_key") - changed = False - - try: - bucket_website = resource_connection.BucketWebsite(bucket_name) - except ClientError as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - try: - website_config = client_connection.get_bucket_website(Bucket=bucket_name) - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchWebsiteConfiguration': - website_config = None - else: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - if website_config is None: - try: - bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) - changed = True - except (ClientError, ParamValidationError) as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - except ValueError as e: - module.fail_json(msg=str(e)) - else: - try: - if (suffix is not None and website_config['IndexDocument']['Suffix'] != suffix) or \ - (error_key is not None and website_config['ErrorDocument']['Key'] != error_key) or \ - (redirect_all_requests is not None and website_config['RedirectAllRequestsTo'] != _create_redirect_dict(redirect_all_requests)): - - try: - bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) - changed = True - except (ClientError, ParamValidationError) as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - except KeyError as e: - try: - bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) - changed = True - except (ClientError, ParamValidationError) as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - except ValueError as e: - module.fail_json(msg=str(e)) - - # Wait 5 secs before getting the website_config again to give it time to update - time.sleep(5) - - website_config = client_connection.get_bucket_website(Bucket=bucket_name) - module.exit_json(changed=changed, **camel_dict_to_snake_dict(website_config)) - - -def disable_bucket_as_website(client_connection, module): - - changed = False - bucket_name = module.params.get("name") - - try: - client_connection.get_bucket_website(Bucket=bucket_name) - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchWebsiteConfiguration': - module.exit_json(changed=changed) - else: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - try: - client_connection.delete_bucket_website(Bucket=bucket_name) - changed = True - except ClientError as e: - module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - - module.exit_json(changed=changed) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(type='str', required=True), - state=dict(type='str', required=True, choices=['present', 'absent']), - suffix=dict(type='str', required=False, default='index.html'), - error_key=dict(type='str', required=False), - redirect_all_requests=dict(type='str', required=False) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['redirect_all_requests', 'suffix'], - ['redirect_all_requests', 'error_key'] - ]) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - client_connection = boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, **aws_connect_params) - resource_connection = boto3_conn(module, conn_type='resource', resource='s3', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - state = module.params.get("state") - - if state == 'present': - enable_or_update_bucket_as_website(client_connection, resource_connection, module) - elif state == 'absent': - disable_bucket_as_website(client_connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/sns.py b/lib/ansible/modules/cloud/amazon/sns.py deleted file mode 100644 index 91ff68b37e..0000000000 --- a/lib/ansible/modules/cloud/amazon/sns.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2014, Michael J. Schultz <mjschultz@gmail.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ -module: sns -short_description: Send Amazon Simple Notification Service messages -description: - - Sends a notification to a topic on your Amazon SNS account. -version_added: 1.6 -author: - - Michael J. Schultz (@mjschultz) - - Paul Arthur (@flowerysong) -options: - msg: - description: - - Default message for subscriptions without a more specific message. - required: true - aliases: [ "default" ] - type: str - subject: - description: - - Message subject - type: str - topic: - description: - - The name or ARN of the topic to publish to. - required: true - type: str - email: - description: - - Message to send to email subscriptions. - type: str - email_json: - description: - - Message to send to email-json subscriptions. - version_added: '2.8' - type: str - sqs: - description: - - Message to send to SQS subscriptions. - type: str - sms: - description: - - Message to send to SMS subscriptions. - type: str - http: - description: - - Message to send to HTTP subscriptions. - type: str - https: - description: - - Message to send to HTTPS subscriptions. - type: str - application: - description: - - Message to send to application subscriptions. - version_added: '2.8' - type: str - lambda: - description: - - Message to send to Lambda subscriptions. - version_added: '2.8' - type: str - message_attributes: - description: - - Dictionary of message attributes. These are optional structured data entries to be sent along to the endpoint. - - This is in AWS's distinct Name/Type/Value format; see example below. - type: dict - message_structure: - description: - - The payload format to use for the message. - - This must be 'json' to support protocol-specific messages (C(http), C(https), C(email), C(sms), C(sqs)). - - It must be 'string' to support I(message_attributes). - default: json - choices: ['json', 'string'] - type: str -extends_documentation_fragment: - - ec2 - - aws -requirements: - - boto3 - - botocore -""" - -EXAMPLES = """ -- name: Send default notification message via SNS - sns: - msg: '{{ inventory_hostname }} has completed the play.' - subject: Deploy complete! - topic: deploy - delegate_to: localhost - -- name: Send notification messages via SNS with short message for SMS - sns: - msg: '{{ inventory_hostname }} has completed the play.' - sms: deployed! - subject: Deploy complete! - topic: deploy - delegate_to: localhost - -- name: Send message with message_attributes - sns: - topic: "deploy" - msg: "message with extra details!" - message_attributes: - channel: - data_type: String - string_value: "mychannel" - color: - data_type: String - string_value: "green" - delegate_to: localhost -""" - -RETURN = """ -msg: - description: Human-readable diagnostic information - returned: always - type: str - sample: OK -message_id: - description: The message ID of the submitted message - returned: when success - type: str - sample: 2f681ef0-6d76-5c94-99b2-4ae3996ce57b -""" - -import json -import traceback - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # Handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule - - -def arn_topic_lookup(module, client, short_topic): - lookup_topic = ':{0}'.format(short_topic) - - try: - paginator = client.get_paginator('list_topics') - topic_iterator = paginator.paginate() - for response in topic_iterator: - for topic in response['Topics']: - if topic['TopicArn'].endswith(lookup_topic): - return topic['TopicArn'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to look up topic ARN') - - return None - - -def main(): - protocols = [ - 'http', - 'https', - 'email', - 'email_json', - 'sms', - 'sqs', - 'application', - 'lambda', - ] - - argument_spec = dict( - msg=dict(required=True, aliases=['default']), - subject=dict(), - topic=dict(required=True), - message_attributes=dict(type='dict'), - message_structure=dict(choices=['json', 'string'], default='json'), - ) - - for p in protocols: - argument_spec[p] = dict() - - module = AnsibleAWSModule(argument_spec=argument_spec) - - sns_kwargs = dict( - Message=module.params['msg'], - Subject=module.params['subject'], - MessageStructure=module.params['message_structure'], - ) - - if module.params['message_attributes']: - if module.params['message_structure'] != 'string': - module.fail_json(msg='message_attributes is only supported when the message_structure is "string".') - sns_kwargs['MessageAttributes'] = module.params['message_attributes'] - - dict_msg = { - 'default': sns_kwargs['Message'] - } - - for p in protocols: - if module.params[p]: - if sns_kwargs['MessageStructure'] != 'json': - module.fail_json(msg='Protocol-specific messages are only supported when message_structure is "json".') - dict_msg[p.replace('_', '-')] = module.params[p] - - client = module.client('sns') - - topic = module.params['topic'] - if ':' in topic: - # Short names can't contain ':' so we'll assume this is the full ARN - sns_kwargs['TopicArn'] = topic - else: - sns_kwargs['TopicArn'] = arn_topic_lookup(module, client, topic) - - if not sns_kwargs['TopicArn']: - module.fail_json(msg='Could not find topic: {0}'.format(topic)) - - if sns_kwargs['MessageStructure'] == 'json': - sns_kwargs['Message'] = json.dumps(dict_msg) - - try: - result = client.publish(**sns_kwargs) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to publish message') - - module.exit_json(msg='OK', message_id=result['MessageId']) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/sns_topic.py b/lib/ansible/modules/cloud/amazon/sns_topic.py deleted file mode 100644 index a247dfdd3f..0000000000 --- a/lib/ansible/modules/cloud/amazon/sns_topic.py +++ /dev/null @@ -1,529 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ -module: sns_topic -short_description: Manages AWS SNS topics and subscriptions -description: - - The M(sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics. - - As of 2.6, this module can be use to subscribe and unsubscribe to topics outside of your AWS account. -version_added: 2.0 -author: - - "Joel Thompson (@joelthompson)" - - "Fernando Jose Pando (@nand0p)" - - "Will Thames (@willthames)" -options: - name: - description: - - The name or ARN of the SNS topic to manage. - required: true - type: str - state: - description: - - Whether to create or destroy an SNS topic. - default: present - choices: ["absent", "present"] - type: str - display_name: - description: - - Display name of the topic. - type: str - policy: - description: - - Policy to apply to the SNS topic. - type: dict - delivery_policy: - description: - - Delivery policy to apply to the SNS topic. - type: dict - subscriptions: - description: - - List of subscriptions to apply to the topic. Note that AWS requires - subscriptions to be confirmed, so you will need to confirm any new - subscriptions. - suboptions: - endpoint: - description: Endpoint of subscription. - required: true - protocol: - description: Protocol of subscription. - required: true - type: list - elements: dict - default: [] - purge_subscriptions: - description: - - "Whether to purge any subscriptions not listed here. NOTE: AWS does not - allow you to purge any PendingConfirmation subscriptions, so if any - exist and would be purged, they are silently skipped. This means that - somebody could come back later and confirm the subscription. Sorry. - Blame Amazon." - default: true - type: bool -extends_documentation_fragment: - - aws - - ec2 -requirements: [ "boto" ] -""" - -EXAMPLES = """ - -- name: Create alarm SNS topic - sns_topic: - name: "alarms" - state: present - display_name: "alarm SNS topic" - delivery_policy: - http: - defaultHealthyRetryPolicy: - minDelayTarget: 2 - maxDelayTarget: 4 - numRetries: 3 - numMaxDelayRetries: 5 - backoffFunction: "<linear|arithmetic|geometric|exponential>" - disableSubscriptionOverrides: True - defaultThrottlePolicy: - maxReceivesPerSecond: 10 - subscriptions: - - endpoint: "my_email_address@example.com" - protocol: "email" - - endpoint: "my_mobile_number" - protocol: "sms" - -""" - -RETURN = ''' -sns_arn: - description: The ARN of the topic you are modifying - type: str - returned: always - sample: "arn:aws:sns:us-east-2:111111111111:my_topic_name" -sns_topic: - description: Dict of sns topic details - type: complex - returned: always - contains: - attributes_set: - description: list of attributes set during this run - returned: always - type: list - sample: [] - check_mode: - description: whether check mode was on - returned: always - type: bool - sample: false - delivery_policy: - description: Delivery policy for the SNS topic - returned: when topic is owned by this AWS account - type: str - sample: > - {"http":{"defaultHealthyRetryPolicy":{"minDelayTarget":20,"maxDelayTarget":20,"numRetries":3,"numMaxDelayRetries":0, - "numNoDelayRetries":0,"numMinDelayRetries":0,"backoffFunction":"linear"},"disableSubscriptionOverrides":false}} - display_name: - description: Display name for SNS topic - returned: when topic is owned by this AWS account - type: str - sample: My topic name - name: - description: Topic name - returned: always - type: str - sample: ansible-test-dummy-topic - owner: - description: AWS account that owns the topic - returned: when topic is owned by this AWS account - type: str - sample: '111111111111' - policy: - description: Policy for the SNS topic - returned: when topic is owned by this AWS account - type: str - sample: > - {"Version":"2012-10-17","Id":"SomePolicyId","Statement":[{"Sid":"ANewSid","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::111111111111:root"}, - "Action":"sns:Subscribe","Resource":"arn:aws:sns:us-east-2:111111111111:ansible-test-dummy-topic","Condition":{"StringEquals":{"sns:Protocol":"email"}}}]} - state: - description: whether the topic is present or absent - returned: always - type: str - sample: present - subscriptions: - description: List of subscribers to the topic in this AWS account - returned: always - type: list - sample: [] - subscriptions_added: - description: List of subscribers added in this run - returned: always - type: list - sample: [] - subscriptions_confirmed: - description: Count of confirmed subscriptions - returned: when topic is owned by this AWS account - type: str - sample: '0' - subscriptions_deleted: - description: Count of deleted subscriptions - returned: when topic is owned by this AWS account - type: str - sample: '0' - subscriptions_existing: - description: List of existing subscriptions - returned: always - type: list - sample: [] - subscriptions_new: - description: List of new subscriptions - returned: always - type: list - sample: [] - subscriptions_pending: - description: Count of pending subscriptions - returned: when topic is owned by this AWS account - type: str - sample: '0' - subscriptions_purge: - description: Whether or not purge_subscriptions was set - returned: always - type: bool - sample: true - topic_arn: - description: ARN of the SNS topic (equivalent to sns_arn) - returned: when topic is owned by this AWS account - type: str - sample: arn:aws:sns:us-east-2:111111111111:ansible-test-dummy-topic - topic_created: - description: Whether the topic was created - returned: always - type: bool - sample: false - topic_deleted: - description: Whether the topic was deleted - returned: always - type: bool - sample: false -''' - -import json -import re -import copy - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import compare_policies, AWSRetry, camel_dict_to_snake_dict - - -class SnsTopicManager(object): - """ Handles SNS Topic creation and destruction """ - - def __init__(self, - module, - name, - state, - display_name, - policy, - delivery_policy, - subscriptions, - purge_subscriptions, - check_mode): - - self.connection = module.client('sns') - self.module = module - self.name = name - self.state = state - self.display_name = display_name - self.policy = policy - self.delivery_policy = delivery_policy - self.subscriptions = subscriptions - self.subscriptions_existing = [] - self.subscriptions_deleted = [] - self.subscriptions_added = [] - self.purge_subscriptions = purge_subscriptions - self.check_mode = check_mode - self.topic_created = False - self.topic_deleted = False - self.topic_arn = None - self.attributes_set = [] - - @AWSRetry.jittered_backoff() - def _list_topics_with_backoff(self): - paginator = self.connection.get_paginator('list_topics') - return paginator.paginate().build_full_result()['Topics'] - - @AWSRetry.jittered_backoff(catch_extra_error_codes=['NotFound']) - def _list_topic_subscriptions_with_backoff(self): - paginator = self.connection.get_paginator('list_subscriptions_by_topic') - return paginator.paginate(TopicArn=self.topic_arn).build_full_result()['Subscriptions'] - - @AWSRetry.jittered_backoff(catch_extra_error_codes=['NotFound']) - def _list_subscriptions_with_backoff(self): - paginator = self.connection.get_paginator('list_subscriptions') - return paginator.paginate().build_full_result()['Subscriptions'] - - def _list_topics(self): - try: - topics = self._list_topics_with_backoff() - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get topic list") - return [t['TopicArn'] for t in topics] - - def _topic_arn_lookup(self): - # topic names cannot have colons, so this captures the full topic name - all_topics = self._list_topics() - lookup_topic = ':%s' % self.name - for topic in all_topics: - if topic.endswith(lookup_topic): - return topic - - def _create_topic(self): - if not self.check_mode: - try: - response = self.connection.create_topic(Name=self.name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't create topic %s" % self.name) - self.topic_arn = response['TopicArn'] - return True - - def _compare_delivery_policies(self, policy_a, policy_b): - _policy_a = copy.deepcopy(policy_a) - _policy_b = copy.deepcopy(policy_b) - # AWS automatically injects disableSubscriptionOverrides if you set an - # http policy - if 'http' in policy_a: - if 'disableSubscriptionOverrides' not in policy_a['http']: - _policy_a['http']['disableSubscriptionOverrides'] = False - if 'http' in policy_b: - if 'disableSubscriptionOverrides' not in policy_b['http']: - _policy_b['http']['disableSubscriptionOverrides'] = False - comparison = (_policy_a != _policy_b) - return comparison - - def _set_topic_attrs(self): - changed = False - try: - topic_attributes = self.connection.get_topic_attributes(TopicArn=self.topic_arn)['Attributes'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get topic attributes for topic %s" % self.topic_arn) - - if self.display_name and self.display_name != topic_attributes['DisplayName']: - changed = True - self.attributes_set.append('display_name') - if not self.check_mode: - try: - self.connection.set_topic_attributes(TopicArn=self.topic_arn, AttributeName='DisplayName', - AttributeValue=self.display_name) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't set display name") - - if self.policy and compare_policies(self.policy, json.loads(topic_attributes['Policy'])): - changed = True - self.attributes_set.append('policy') - if not self.check_mode: - try: - self.connection.set_topic_attributes(TopicArn=self.topic_arn, AttributeName='Policy', - AttributeValue=json.dumps(self.policy)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't set topic policy") - - if self.delivery_policy and ('DeliveryPolicy' not in topic_attributes or - self._compare_delivery_policies(self.delivery_policy, json.loads(topic_attributes['DeliveryPolicy']))): - changed = True - self.attributes_set.append('delivery_policy') - if not self.check_mode: - try: - self.connection.set_topic_attributes(TopicArn=self.topic_arn, AttributeName='DeliveryPolicy', - AttributeValue=json.dumps(self.delivery_policy)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't set topic delivery policy") - return changed - - def _canonicalize_endpoint(self, protocol, endpoint): - if protocol == 'sms': - return re.sub('[^0-9]*', '', endpoint) - return endpoint - - def _set_topic_subs(self): - changed = False - subscriptions_existing_list = set() - desired_subscriptions = [(sub['protocol'], - self._canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in - self.subscriptions] - - for sub in self._list_topic_subscriptions(): - sub_key = (sub['Protocol'], sub['Endpoint']) - subscriptions_existing_list.add(sub_key) - if (self.purge_subscriptions and sub_key not in desired_subscriptions and - sub['SubscriptionArn'] not in ('PendingConfirmation', 'Deleted')): - changed = True - self.subscriptions_deleted.append(sub_key) - if not self.check_mode: - try: - self.connection.unsubscribe(SubscriptionArn=sub['SubscriptionArn']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't unsubscribe from topic") - - for protocol, endpoint in set(desired_subscriptions).difference(subscriptions_existing_list): - changed = True - self.subscriptions_added.append((protocol, endpoint)) - if not self.check_mode: - try: - self.connection.subscribe(TopicArn=self.topic_arn, Protocol=protocol, Endpoint=endpoint) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't subscribe to topic %s" % self.topic_arn) - return changed - - def _list_topic_subscriptions(self): - try: - return self._list_topic_subscriptions_with_backoff() - except is_boto3_error_code('AuthorizationError'): - try: - # potentially AuthorizationError when listing subscriptions for third party topic - return [sub for sub in self._list_subscriptions_with_backoff() - if sub['TopicArn'] == self.topic_arn] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't get subscriptions list for topic %s" % self.topic_arn) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - self.module.fail_json_aws(e, msg="Couldn't get subscriptions list for topic %s" % self.topic_arn) - - def _delete_subscriptions(self): - # NOTE: subscriptions in 'PendingConfirmation' timeout in 3 days - # https://forums.aws.amazon.com/thread.jspa?threadID=85993 - subscriptions = self._list_topic_subscriptions() - if not subscriptions: - return False - for sub in subscriptions: - if sub['SubscriptionArn'] not in ('PendingConfirmation', 'Deleted'): - self.subscriptions_deleted.append(sub['SubscriptionArn']) - if not self.check_mode: - try: - self.connection.unsubscribe(SubscriptionArn=sub['SubscriptionArn']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't unsubscribe from topic") - return True - - def _delete_topic(self): - self.topic_deleted = True - if not self.check_mode: - try: - self.connection.delete_topic(TopicArn=self.topic_arn) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self.module.fail_json_aws(e, msg="Couldn't delete topic %s" % self.topic_arn) - return True - - def _name_is_arn(self): - return self.name.startswith('arn:') - - def ensure_ok(self): - changed = False - if self._name_is_arn(): - self.topic_arn = self.name - else: - self.topic_arn = self._topic_arn_lookup() - if not self.topic_arn: - changed = self._create_topic() - if self.topic_arn in self._list_topics(): - changed |= self._set_topic_attrs() - elif self.display_name or self.policy or self.delivery_policy: - self.module.fail_json(msg="Cannot set display name, policy or delivery policy for SNS topics not owned by this account") - changed |= self._set_topic_subs() - return changed - - def ensure_gone(self): - changed = False - if self._name_is_arn(): - self.topic_arn = self.name - else: - self.topic_arn = self._topic_arn_lookup() - if self.topic_arn: - if self.topic_arn not in self._list_topics(): - self.module.fail_json(msg="Cannot use state=absent with third party ARN. Use subscribers=[] to unsubscribe") - changed = self._delete_subscriptions() - changed |= self._delete_topic() - return changed - - def get_info(self): - info = { - 'name': self.name, - 'state': self.state, - 'subscriptions_new': self.subscriptions, - 'subscriptions_existing': self.subscriptions_existing, - 'subscriptions_deleted': self.subscriptions_deleted, - 'subscriptions_added': self.subscriptions_added, - 'subscriptions_purge': self.purge_subscriptions, - 'check_mode': self.check_mode, - 'topic_created': self.topic_created, - 'topic_deleted': self.topic_deleted, - 'attributes_set': self.attributes_set, - } - if self.state != 'absent': - if self.topic_arn in self._list_topics(): - info.update(camel_dict_to_snake_dict(self.connection.get_topic_attributes(TopicArn=self.topic_arn)['Attributes'])) - info['delivery_policy'] = info.pop('effective_delivery_policy') - info['subscriptions'] = [camel_dict_to_snake_dict(sub) for sub in self._list_topic_subscriptions()] - - return info - - -def main(): - argument_spec = dict( - name=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - display_name=dict(), - policy=dict(type='dict'), - delivery_policy=dict(type='dict'), - subscriptions=dict(default=[], type='list'), - purge_subscriptions=dict(type='bool', default=True), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - supports_check_mode=True) - - name = module.params.get('name') - state = module.params.get('state') - display_name = module.params.get('display_name') - policy = module.params.get('policy') - delivery_policy = module.params.get('delivery_policy') - subscriptions = module.params.get('subscriptions') - purge_subscriptions = module.params.get('purge_subscriptions') - check_mode = module.check_mode - - sns_topic = SnsTopicManager(module, - name, - state, - display_name, - policy, - delivery_policy, - subscriptions, - purge_subscriptions, - check_mode) - - if state == 'present': - changed = sns_topic.ensure_ok() - - elif state == 'absent': - changed = sns_topic.ensure_gone() - - sns_facts = dict(changed=changed, - sns_arn=sns_topic.topic_arn, - sns_topic=sns_topic.get_info()) - - module.exit_json(**sns_facts) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/sqs_queue.py b/lib/ansible/modules/cloud/amazon/sqs_queue.py deleted file mode 100644 index 6e8b680190..0000000000 --- a/lib/ansible/modules/cloud/amazon/sqs_queue.py +++ /dev/null @@ -1,481 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: sqs_queue -short_description: Creates or deletes AWS SQS queues. -description: - - Create or delete AWS SQS queues. - - Update attributes on existing queues. -version_added: "2.0" -author: - - Alan Loi (@loia) - - Fernando Jose Pando (@nand0p) - - Nadir Lloret (@nadirollo) - - Dennis Podkovyrin (@sbj-ss) -requirements: - - boto3 -options: - state: - description: - - Create or delete the queue. - choices: ['present', 'absent'] - default: 'present' - type: str - name: - description: - - Name of the queue. - required: true - type: str - queue_type: - description: - - Standard or FIFO queue. - - I(queue_type) can only be set at queue creation and will otherwise be - ignored. - choices: ['standard', 'fifo'] - default: 'standard' - version_added: "2.10" - type: str - visibility_timeout: - description: - - The default visibility timeout in seconds. - aliases: [default_visibility_timeout] - type: int - message_retention_period: - description: - - The message retention period in seconds. - type: int - maximum_message_size: - description: - - The maximum message size in bytes. - type: int - delay_seconds: - description: - - The delivery delay in seconds. - aliases: [delivery_delay] - type: int - receive_message_wait_time_seconds: - description: - - The receive message wait time in seconds. - aliases: [receive_message_wait_time] - type: int - policy: - description: - - The JSON dict policy to attach to queue. - version_added: "2.1" - type: dict - redrive_policy: - description: - - JSON dict with the redrive_policy (see example). - version_added: "2.2" - type: dict - kms_master_key_id: - description: - - The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. - version_added: "2.10" - type: str - kms_data_key_reuse_period_seconds: - description: - - The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. - aliases: [kms_data_key_reuse_period] - version_added: "2.10" - type: int - content_based_deduplication: - type: bool - description: Enables content-based deduplication. Used for FIFOs only. - version_added: "2.10" - default: false - tags: - description: - - Tag dict to apply to the queue (requires botocore 1.5.40 or above). - - To remove all tags set I(tags={}) and I(purge_tags=true). - version_added: "2.10" - type: dict - purge_tags: - description: - - Remove tags not listed in I(tags). - type: bool - default: false - version_added: "2.10" -extends_documentation_fragment: - - aws - - ec2 -""" - -RETURN = ''' -content_based_deduplication: - description: Enables content-based deduplication. Used for FIFOs only. - type: bool - returned: always - sample: True -visibility_timeout: - description: The default visibility timeout in seconds. - type: int - returned: always - sample: 30 -delay_seconds: - description: The delivery delay in seconds. - type: int - returned: always - sample: 0 -kms_master_key_id: - description: The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. - type: str - returned: always - sample: alias/MyAlias -kms_data_key_reuse_period_seconds: - description: The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. - type: int - returned: always - sample: 300 -maximum_message_size: - description: The maximum message size in bytes. - type: int - returned: always - sample: 262144 -message_retention_period: - description: The message retention period in seconds. - type: int - returned: always - sample: 345600 -name: - description: Name of the SQS Queue - type: str - returned: always - sample: "queuename-987d2de0" -queue_arn: - description: The queue's Amazon resource name (ARN). - type: str - returned: on success - sample: 'arn:aws:sqs:us-east-1:199999999999:queuename-987d2de0' -queue_url: - description: URL to access the queue - type: str - returned: on success - sample: 'https://queue.amazonaws.com/123456789012/MyQueue' -receive_message_wait_time_seconds: - description: The receive message wait time in seconds. - type: int - returned: always - sample: 0 -region: - description: Region that the queue was created within - type: str - returned: always - sample: 'us-east-1' -tags: - description: List of queue tags - type: dict - returned: always - sample: '{"Env": "prod"}' -''' - -EXAMPLES = ''' -# Create SQS queue with redrive policy -- sqs_queue: - name: my-queue - region: ap-southeast-2 - default_visibility_timeout: 120 - message_retention_period: 86400 - maximum_message_size: 1024 - delivery_delay: 30 - receive_message_wait_time: 20 - policy: "{{ json_dict }}" - redrive_policy: - maxReceiveCount: 5 - deadLetterTargetArn: arn:aws:sqs:eu-west-1:123456789012:my-dead-queue - -# Drop redrive policy -- sqs_queue: - name: my-queue - region: ap-southeast-2 - redrive_policy: {} - -# Create FIFO queue -- sqs_queue: - name: fifo-queue - region: ap-southeast-2 - queue_type: fifo - content_based_deduplication: yes - -# Tag queue -- sqs_queue: - name: fifo-queue - region: ap-southeast-2 - tags: - example: SomeValue - -# Configure Encryption, automatically uses a new data key every hour -- sqs_queue: - name: fifo-queue - region: ap-southeast-2 - kms_master_key_id: alias/MyQueueKey - kms_data_key_reuse_period_seconds: 3600 - -# Delete SQS queue -- sqs_queue: - name: my-queue - region: ap-southeast-2 - state: absent -''' - -import json -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict, compare_aws_tags, snake_dict_to_camel_dict, compare_policies - -try: - from botocore.exceptions import BotoCoreError, ClientError, ParamValidationError -except ImportError: - pass # handled by AnsibleAWSModule - - -def get_queue_name(module, is_fifo=False): - name = module.params.get('name') - if not is_fifo or name.endswith('.fifo'): - return name - return name + '.fifo' - - -# NonExistentQueue is explicitly expected when a queue doesn't exist -@AWSRetry.jittered_backoff() -def get_queue_url(client, name): - try: - return client.get_queue_url(QueueName=name)['QueueUrl'] - except ClientError as e: - if e.response['Error']['Code'] == 'AWS.SimpleQueueService.NonExistentQueue': - return None - raise - - -def describe_queue(client, queue_url): - """ - Description a queue in snake format - """ - attributes = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['All'], aws_retry=True)['Attributes'] - description = dict(attributes) - description.pop('Policy', None) - description.pop('RedrivePolicy', None) - description = camel_dict_to_snake_dict(description) - description['policy'] = attributes.get('Policy', None) - description['redrive_policy'] = attributes.get('RedrivePolicy', None) - - # Boto3 returns everything as a string, convert them back to integers/dicts if - # that's what we expected. - for key, value in description.items(): - if value is None: - continue - - if key in ['policy', 'redrive_policy']: - policy = json.loads(value) - description[key] = policy - continue - - if key == 'content_based_deduplication': - try: - description[key] = bool(value) - except (TypeError, ValueError): - pass - - try: - if value == str(int(value)): - description[key] = int(value) - except (TypeError, ValueError): - pass - - return description - - -def create_or_update_sqs_queue(client, module): - is_fifo = (module.params.get('queue_type') == 'fifo') - queue_name = get_queue_name(module, is_fifo) - result = dict( - name=queue_name, - region=module.params.get('region'), - changed=False, - ) - - queue_url = get_queue_url(client, queue_name) - result['queue_url'] = queue_url - - if not queue_url: - create_attributes = {'FifoQueue': 'true'} if is_fifo else {} - result['changed'] = True - if module.check_mode: - return result - queue_url = client.create_queue(QueueName=queue_name, Attributes=create_attributes, aws_retry=True)['QueueUrl'] - - changed, arn = update_sqs_queue(module, client, queue_url) - result['changed'] |= changed - result['queue_arn'] = arn - - changed, tags = update_tags(client, queue_url, module) - result['changed'] |= changed - result['tags'] = tags - - result.update(describe_queue(client, queue_url)) - - COMPATABILITY_KEYS = dict( - delay_seconds='delivery_delay', - receive_message_wait_time_seconds='receive_message_wait_time', - visibility_timeout='default_visibility_timeout', - kms_data_key_reuse_period_seconds='kms_data_key_reuse_period', - ) - for key in list(result.keys()): - - # The return values changed between boto and boto3, add the old keys too - # for backwards compatibility - return_name = COMPATABILITY_KEYS.get(key) - if return_name: - result[return_name] = result.get(key) - - return result - - -def update_sqs_queue(module, client, queue_url): - check_mode = module.check_mode - changed = False - existing_attributes = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['All'], aws_retry=True)['Attributes'] - new_attributes = snake_dict_to_camel_dict(module.params, capitalize_first=True) - attributes_to_set = dict() - - # Boto3 SQS deals with policies as strings, we want to deal with them as - # dicts - if module.params.get('policy') is not None: - policy = module.params.get('policy') - current_value = existing_attributes.get('Policy', '{}') - current_policy = json.loads(current_value) - if compare_policies(current_policy, policy): - attributes_to_set['Policy'] = json.dumps(policy) - changed = True - if module.params.get('redrive_policy') is not None: - policy = module.params.get('redrive_policy') - current_value = existing_attributes.get('RedrivePolicy', '{}') - current_policy = json.loads(current_value) - if compare_policies(current_policy, policy): - attributes_to_set['RedrivePolicy'] = json.dumps(policy) - changed = True - - for attribute, value in existing_attributes.items(): - # We handle these as a special case because they're IAM policies - if attribute in ['Policy', 'RedrivePolicy']: - continue - - if attribute not in new_attributes.keys(): - continue - - if new_attributes.get(attribute) is None: - continue - - new_value = new_attributes[attribute] - - if isinstance(new_value, bool): - new_value = str(new_value).lower() - existing_value = str(existing_value).lower() - - if new_value == value: - continue - - # Boto3 expects strings - attributes_to_set[attribute] = str(new_value) - changed = True - - if changed and not check_mode: - client.set_queue_attributes(QueueUrl=queue_url, Attributes=attributes_to_set, aws_retry=True) - - return changed, existing_attributes.get('queue_arn'), - - -def delete_sqs_queue(client, module): - is_fifo = (module.params.get('queue_type') == 'fifo') - queue_name = get_queue_name(module, is_fifo) - result = dict( - name=queue_name, - region=module.params.get('region'), - changed=False - ) - - queue_url = get_queue_url(client, queue_name) - if not queue_url: - return result - - result['changed'] = bool(queue_url) - if not module.check_mode: - AWSRetry.jittered_backoff()(client.delete_queue)(QueueUrl=queue_url) - - return result - - -def update_tags(client, queue_url, module): - new_tags = module.params.get('tags') - purge_tags = module.params.get('purge_tags') - if new_tags is None: - return False, {} - - try: - existing_tags = client.list_queue_tags(QueueUrl=queue_url, aws_retry=True)['Tags'] - except (ClientError, KeyError) as e: - existing_tags = {} - - tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, new_tags, purge_tags=purge_tags) - - if not module.check_mode: - if tags_to_remove: - client.untag_queue(QueueUrl=queue_url, TagKeys=tags_to_remove, aws_retry=True) - if tags_to_add: - client.tag_queue(QueueUrl=queue_url, Tags=tags_to_add) - existing_tags = client.list_queue_tags(QueueUrl=queue_url, aws_retry=True).get('Tags', {}) - else: - existing_tags = new_tags - - changed = bool(tags_to_remove) or bool(tags_to_add) - return changed, existing_tags - - -def main(): - - argument_spec = dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - name=dict(type='str', required=True), - queue_type=dict(type='str', default='standard', choices=['standard', 'fifo']), - delay_seconds=dict(type='int', aliases=['delivery_delay']), - maximum_message_size=dict(type='int'), - message_retention_period=dict(type='int'), - policy=dict(type='dict'), - receive_message_wait_time_seconds=dict(type='int', aliases=['receive_message_wait_time']), - redrive_policy=dict(type='dict'), - visibility_timeout=dict(type='int', aliases=['default_visibility_timeout']), - kms_master_key_id=dict(type='str'), - kms_data_key_reuse_period_seconds=dict(type='int', aliases=['kms_data_key_reuse_period']), - content_based_deduplication=dict(type='bool'), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=False), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - state = module.params.get('state') - retry_decorator = AWSRetry.jittered_backoff(catch_extra_error_codes=['AWS.SimpleQueueService.NonExistentQueue']) - try: - client = module.client('sqs', retry_decorator=retry_decorator) - if state == 'present': - result = create_or_update_sqs_queue(client, module) - elif state == 'absent': - result = delete_sqs_queue(client, module) - except (BotoCoreError, ClientError, ParamValidationError) as e: - module.fail_json_aws(e, msg='Failed to control sqs queue') - else: - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/sts_assume_role.py b/lib/ansible/modules/cloud/amazon/sts_assume_role.py deleted file mode 100644 index cd82a549cb..0000000000 --- a/lib/ansible/modules/cloud/amazon/sts_assume_role.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: sts_assume_role -short_description: Assume a role using AWS Security Token Service and obtain temporary credentials -description: - - Assume a role using AWS Security Token Service and obtain temporary credentials. -version_added: "2.0" -author: - - Boris Ekelchik (@bekelchik) - - Marek Piatek (@piontas) -options: - role_arn: - description: - - The Amazon Resource Name (ARN) of the role that the caller is - assuming U(https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs). - required: true - type: str - role_session_name: - description: - - Name of the role's session - will be used by CloudTrail. - required: true - type: str - policy: - description: - - Supplemental policy to use in addition to assumed role's policies. - type: str - duration_seconds: - description: - - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 43200 seconds (12 hours). - - The max depends on the IAM role's sessions duration setting. - - By default, the value is set to 3600 seconds. - type: int - external_id: - description: - - A unique identifier that is used by third parties to assume a role in their customers' accounts. - type: str - mfa_serial_number: - description: - - The identification number of the MFA device that is associated with the user who is making the AssumeRole call. - type: str - mfa_token: - description: - - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. - type: str -notes: - - In order to use the assumed role in a following playbook task you must pass the access_key, access_secret and access_token. -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore - - python >= 2.6 -''' - -RETURN = ''' -sts_creds: - description: The temporary security credentials, which include an access key ID, a secret access key, and a security (or session) token - returned: always - type: dict - sample: - access_key: XXXXXXXXXXXXXXXXXXXX - expiration: 2017-11-11T11:11:11+00:00 - secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - session_token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -sts_user: - description: The Amazon Resource Name (ARN) and the assumed role ID - returned: always - type: dict - sample: - assumed_role_id: arn:aws:sts::123456789012:assumed-role/demo/Bob - arn: ARO123EXAMPLE123:Bob -changed: - description: True if obtaining the credentials succeeds - type: bool - returned: always -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Assume an existing role (more details: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) -- sts_assume_role: - role_arn: "arn:aws:iam::123456789012:role/someRole" - role_session_name: "someRoleSession" - register: assumed_role - -# Use the assumed role above to tag an instance in account 123456789012 -- ec2_tag: - aws_access_key: "{{ assumed_role.sts_creds.access_key }}" - aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}" - security_token: "{{ assumed_role.sts_creds.session_token }}" - resource: i-xyzxyz01 - state: present - tags: - MyNewTag: value - -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, ParamValidationError -except ImportError: - pass # caught by AnsibleAWSModule - - -def _parse_response(response): - credentials = response.get('Credentials', {}) - user = response.get('AssumedRoleUser', {}) - - sts_cred = { - 'access_key': credentials.get('AccessKeyId'), - 'secret_key': credentials.get('SecretAccessKey'), - 'session_token': credentials.get('SessionToken'), - 'expiration': credentials.get('Expiration') - - } - sts_user = camel_dict_to_snake_dict(user) - return sts_cred, sts_user - - -def assume_role_policy(connection, module): - params = { - 'RoleArn': module.params.get('role_arn'), - 'RoleSessionName': module.params.get('role_session_name'), - 'Policy': module.params.get('policy'), - 'DurationSeconds': module.params.get('duration_seconds'), - 'ExternalId': module.params.get('external_id'), - 'SerialNumber': module.params.get('mfa_serial_number'), - 'TokenCode': module.params.get('mfa_token') - } - changed = False - - kwargs = dict((k, v) for k, v in params.items() if v is not None) - - try: - response = connection.assume_role(**kwargs) - changed = True - except (ClientError, ParamValidationError) as e: - module.fail_json_aws(e) - - sts_cred, sts_user = _parse_response(response) - module.exit_json(changed=changed, sts_creds=sts_cred, sts_user=sts_user) - - -def main(): - argument_spec = dict( - role_arn=dict(required=True), - role_session_name=dict(required=True), - duration_seconds=dict(required=False, default=None, type='int'), - external_id=dict(required=False, default=None), - policy=dict(required=False, default=None), - mfa_serial_number=dict(required=False, default=None), - mfa_token=dict(required=False, default=None) - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - - connection = module.client('sts') - - assume_role_policy(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/sts_session_token.py b/lib/ansible/modules/cloud/amazon/sts_session_token.py deleted file mode 100644 index 4606448dbf..0000000000 --- a/lib/ansible/modules/cloud/amazon/sts_session_token.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: sts_session_token -short_description: Obtain a session token from the AWS Security Token Service -description: - - Obtain a session token from the AWS Security Token Service. -version_added: "2.2" -author: Victor Costan (@pwnall) -options: - duration_seconds: - description: - - The duration, in seconds, of the session token. - See U(https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html#API_GetSessionToken_RequestParameters) - for acceptable and default values. - type: int - mfa_serial_number: - description: - - The identification number of the MFA device that is associated with the user who is making the GetSessionToken call. - type: str - mfa_token: - description: - - The value provided by the MFA device, if the trust policy of the user requires MFA. - type: str -notes: - - In order to use the session token in a following playbook task you must pass the I(access_key), I(access_secret) and I(access_token). -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto3 - - botocore - - python >= 2.6 -''' - -RETURN = """ -sts_creds: - description: The Credentials object returned by the AWS Security Token Service - returned: always - type: list - sample: - access_key: ASXXXXXXXXXXXXXXXXXX - expiration: "2016-04-08T11:59:47+00:00" - secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - session_token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -changed: - description: True if obtaining the credentials succeeds - type: bool - returned: always -""" - - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Get a session token (more details: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html) -sts_session_token: - duration_seconds: 3600 -register: session_credentials - -# Use the session token obtained above to tag an instance in account 123456789012 -ec2_tag: - aws_access_key: "{{ session_credentials.sts_creds.access_key }}" - aws_secret_key: "{{ session_credentials.sts_creds.secret_key }}" - security_token: "{{ session_credentials.sts_creds.session_token }}" - resource: i-xyzxyz01 - state: present - tags: - MyNewTag: value - -''' - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info - - -def normalize_credentials(credentials): - access_key = credentials.get('AccessKeyId', None) - secret_key = credentials.get('SecretAccessKey', None) - session_token = credentials.get('SessionToken', None) - expiration = credentials.get('Expiration', None) - return { - 'access_key': access_key, - 'secret_key': secret_key, - 'session_token': session_token, - 'expiration': expiration - } - - -def get_session_token(connection, module): - duration_seconds = module.params.get('duration_seconds') - mfa_serial_number = module.params.get('mfa_serial_number') - mfa_token = module.params.get('mfa_token') - changed = False - - args = {} - if duration_seconds is not None: - args['DurationSeconds'] = duration_seconds - if mfa_serial_number is not None: - args['SerialNumber'] = mfa_serial_number - if mfa_token is not None: - args['TokenCode'] = mfa_token - - try: - response = connection.get_session_token(**args) - changed = True - except ClientError as e: - module.fail_json(msg=e) - - credentials = normalize_credentials(response.get('Credentials', {})) - module.exit_json(changed=changed, sts_creds=credentials) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - duration_seconds=dict(required=False, default=None, type='int'), - mfa_serial_number=dict(required=False, default=None), - mfa_token=dict(required=False, default=None) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required.') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if region: - connection = boto3_conn(module, conn_type='client', resource='sts', region=region, endpoint=ec2_url, **aws_connect_kwargs) - else: - module.fail_json(msg="region must be specified") - - get_session_token(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/plugins/connection/aws_ssm.py b/lib/ansible/plugins/connection/aws_ssm.py deleted file mode 100644 index 359db76f3d..0000000000 --- a/lib/ansible/plugins/connection/aws_ssm.py +++ /dev/null @@ -1,557 +0,0 @@ -# Based on the ssh connection plugin by Michael DeHaan -# -# Copyright: (c) 2018, Pat Sharkey <psharkey@cleo.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = """ -author: -- Pat Sharkey (@psharkey) <psharkey@cleo.com> -- HanumanthaRao MVL (@hanumantharaomvl) <hanumanth@flux7.com> -- Gaurav Ashtikar (@gau1991 )<gaurav.ashtikar@flux7.com> -connection: aws_ssm -short_description: execute via AWS Systems Manager -description: -- This connection plugin allows ansible to execute tasks on an EC2 instance via the aws ssm CLI. -version_added: "2.10" -requirements: -- The remote EC2 instance must be running the AWS Systems Manager Agent (SSM Agent). -- The control machine must have the aws session manager plugin installed. -- The remote EC2 linux instance must have the curl installed. -options: - instance_id: - description: The EC2 instance ID. - vars: - - name: ansible_aws_ssm_instance_id - region: - description: The region the EC2 instance is located. - vars: - - name: ansible_aws_ssm_region - default: 'us-east-1' - bucket_name: - description: The name of the S3 bucket used for file transfers. - vars: - - name: ansible_aws_ssm_bucket_name - plugin: - description: This defines the location of the session-manager-plugin binary. - vars: - - name: ansible_aws_ssm_plugin - default: '/usr/local/bin/session-manager-plugin' - retries: - description: Number of attempts to connect. - default: 3 - type: integer - vars: - - name: ansible_aws_ssm_retries - timeout: - description: Connection timeout seconds. - default: 60 - type: integer - vars: - - name: ansible_aws_ssm_timeout -""" - -EXAMPLES = r''' - -# Stop Spooler Process on Windows Instances -- name: Stop Spooler Service on Windows Instances - vars: - ansible_connection: aws_ssm - ansible_shell_type: powershell - ansible_aws_ssm_bucket_name: nameofthebucket - ansible_aws_ssm_region: us-east-1 - tasks: - - name: Stop spooler service - win_service: - name: spooler - state: stopped - -# Install a Nginx Package on Linux Instance -- name: Install a Nginx Package - vars: - ansible_connection: aws_ssm - ansible_aws_ssm_bucket_name: nameofthebucket - ansible_aws_ssm_region: us-west-2 - tasks: - - name: Install a Nginx Package - yum: - name: nginx - state: present - -# Create a directory in Windows Instances -- name: Create a directory in Windows Instance - vars: - ansible_connection: aws_ssm - ansible_shell_type: powershell - ansible_aws_ssm_bucket_name: nameofthebucket - ansible_aws_ssm_region: us-east-1 - tasks: - - name: Create a Directory - win_file: - path: C:\Windows\temp - state: directory - -# Making use of Dynamic Inventory Plugin -# ======================================= -# aws_ec2.yml (Dynamic Inventory - Linux) -# This will return the Instance IDs matching the filter -#plugin: aws_ec2 -#regions: -# - us-east-1 -#hostnames: -# - instance-id -#filters: -# tag:SSMTag: ssmlinux -# ----------------------- -- name: install aws-cli - hosts: all - gather_facts: false - vars: - ansible_connection: aws_ssm - ansible_aws_ssm_bucket_name: nameofthebucket - ansible_aws_ssm_region: us-east-1 - tasks: - - name: aws-cli - raw: yum install -y awscli - tags: aws-cli -# Execution: ansible-playbook linux.yaml -i aws_ec2.yml -# The playbook tasks will get executed on the instance ids returned from the dynamic inventory plugin using ssm connection. -# ===================================================== -# aws_ec2.yml (Dynamic Inventory - Windows) -#plugin: aws_ec2 -#regions: -# - us-east-1 -#hostnames: -# - instance-id -#filters: -# tag:SSMTag: ssmwindows -# ----------------------- -- name: Create a dir. - hosts: all - gather_facts: false - vars: - ansible_connection: aws_ssm - ansible_shell_type: powershell - ansible_aws_ssm_bucket_name: nameofthebucket - ansible_aws_ssm_region: us-east-1 - tasks: - - name: Create the directory - win_file: - path: C:\Temp\SSM_Testing5 - state: directory -# Execution: ansible-playbook win_file.yaml -i aws_ec2.yml -# The playbook tasks will get executed on the instance ids returned from the dynamic inventory plugin using ssm connection. -''' - -import os -import getpass -import json -import os -import pty -import random -import re -import select -import string -import subprocess -import time - -try: - import boto3 - HAS_BOTO_3 = True -except ImportError as e: - HAS_BOTO_3_ERROR = str(e) - HAS_BOTO_3 = False - -from functools import wraps -from ansible import constants as C -from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound -from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.six import PY3 -from ansible.module_utils.six.moves import xrange -from ansible.module_utils._text import to_bytes, to_native, to_text -from ansible.plugins.connection import ConnectionBase -from ansible.plugins.shell.powershell import _common_args -from ansible.utils.display import Display - -display = Display() - - -def _ssm_retry(func): - """ - Decorator to retry in the case of a connection failure - Will retry if: - * an exception is caught - Will not retry if - * remaining_tries is <2 - * retries limit reached - """ - @wraps(func) - def wrapped(self, *args, **kwargs): - remaining_tries = int(self.get_option('retries')) + 1 - cmd_summary = "%s..." % args[0] - for attempt in range(remaining_tries): - cmd = args[0] - - try: - return_tuple = func(self, *args, **kwargs) - display.vvv(return_tuple, host=self.host) - break - - except (AnsibleConnectionFailure, Exception) as e: - if attempt == remaining_tries - 1: - raise - else: - pause = 2 ** attempt - 1 - if pause > 30: - pause = 30 - - if isinstance(e, AnsibleConnectionFailure): - msg = "ssm_retry: attempt: %d, cmd (%s), pausing for %d seconds" % (attempt, cmd_summary, pause) - else: - msg = "ssm_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt, e, cmd_summary, pause) - - display.vv(msg, host=self.host) - - time.sleep(pause) - - # Do not attempt to reuse the existing session on retries - self.close() - - continue - - return return_tuple - return wrapped - - -def chunks(lst, n): - """Yield successive n-sized chunks from lst.""" - for i in range(0, len(lst), n): - yield lst[i:i + n] - - -class Connection(ConnectionBase): - ''' AWS SSM based connections ''' - - transport = 'aws_ssm' - allow_executable = False - allow_extras = True - has_pipelining = False - is_windows = False - _client = None - _session = None - _stdout = None - _session_id = '' - _timeout = False - MARK_LENGTH = 26 - - def __init__(self, *args, **kwargs): - if not HAS_BOTO_3: - raise AnsibleError('{0}: {1}'.format(missing_required_lib("boto3"), HAS_BOTO_3_ERROR)) - - super(Connection, self).__init__(*args, **kwargs) - self.host = self._play_context.remote_addr - - if getattr(self._shell, "SHELL_FAMILY", '') == 'powershell': - self.delegate = None - self.has_native_async = True - self.always_pipeline_modules = True - self.module_implementation_preferences = ('.ps1', '.exe', '') - self.protocol = None - self.shell_id = None - self._shell_type = 'powershell' - self.is_windows = True - - def _connect(self): - ''' connect to the host via ssm ''' - - self._play_context.remote_user = getpass.getuser() - - if not self._session_id: - self.start_session() - return self - - def start_session(self): - ''' start ssm session ''' - - if self.get_option('instance_id') is None: - self.instance_id = self.host - else: - self.instance_id = self.get_option('instance_id') - - display.vvv(u"ESTABLISH SSM CONNECTION TO: {0}".format(self.instance_id), host=self.host) - - executable = self.get_option('plugin') - if not os.path.exists(to_bytes(executable, errors='surrogate_or_strict')): - raise AnsibleError("failed to find the executable specified %s." - " Please verify if the executable exists and re-try." % executable) - - profile_name = '' - region_name = self.get_option('region') - ssm_parameters = dict() - - client = boto3.client('ssm', region_name=region_name) - self._client = client - response = client.start_session(Target=self.instance_id, Parameters=ssm_parameters) - self._session_id = response['SessionId'] - - cmd = [ - executable, - json.dumps(response), - region_name, - "StartSession", - profile_name, - json.dumps({"Target": self.instance_id}), - client.meta.endpoint_url - ] - - display.vvvv(u"SSM COMMAND: {0}".format(to_text(cmd)), host=self.host) - - stdout_r, stdout_w = pty.openpty() - session = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=stdout_w, - stderr=subprocess.PIPE, - close_fds=True, - bufsize=0, - ) - - os.close(stdout_w) - self._stdout = os.fdopen(stdout_r, 'rb', 0) - self._session = session - self._poll_stdout = select.poll() - self._poll_stdout.register(self._stdout, select.POLLIN) - - # Disable command echo and prompt. - self._prepare_terminal() - - display.vvv(u"SSM CONNECTION ID: {0}".format(self._session_id), host=self.host) - - return session - - @_ssm_retry - def exec_command(self, cmd, in_data=None, sudoable=True): - ''' run a command on the ssm host ''' - - super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) - - display.vvv(u"EXEC {0}".format(to_text(cmd)), host=self.host) - - session = self._session - - mark_begin = "".join([random.choice(string.ascii_letters) for i in xrange(self.MARK_LENGTH)]) - if self.is_windows: - mark_start = mark_begin + " $LASTEXITCODE" - else: - mark_start = mark_begin - mark_end = "".join([random.choice(string.ascii_letters) for i in xrange(self.MARK_LENGTH)]) - - # Wrap command in markers accordingly for the shell used - cmd = self._wrap_command(cmd, sudoable, mark_start, mark_end) - - self._flush_stderr(session) - - for chunk in chunks(cmd, 1024): - session.stdin.write(to_bytes(chunk, errors='surrogate_or_strict')) - - # Read stdout between the markers - stdout = '' - win_line = '' - begin = False - stop_time = int(round(time.time())) + self.get_option('timeout') - while session.poll() is None: - remaining = stop_time - int(round(time.time())) - if remaining < 1: - self._timeout = True - display.vvvv(u"EXEC timeout stdout: {0}".format(to_text(stdout)), host=self.host) - raise AnsibleConnectionFailure("SSM exec_command timeout on host: %s" - % self.instance_id) - if self._poll_stdout.poll(1000): - line = self._filter_ansi(self._stdout.readline()) - display.vvvv(u"EXEC stdout line: {0}".format(to_text(line)), host=self.host) - else: - display.vvvv(u"EXEC remaining: {0}".format(remaining), host=self.host) - continue - - if not begin and self.is_windows: - win_line = win_line + line - line = win_line - - if mark_start in line: - begin = True - if not line.startswith(mark_start): - stdout = '' - continue - if begin: - if mark_end in line: - display.vvvv(u"POST_PROCESS: {0}".format(to_text(stdout)), host=self.host) - returncode, stdout = self._post_process(stdout, mark_begin) - break - else: - stdout = stdout + line - - stderr = self._flush_stderr(session) - - return (returncode, stdout, stderr) - - def _prepare_terminal(self): - ''' perform any one-time terminal settings ''' - - if not self.is_windows: - cmd = "stty -echo\n" + "PS1=''\n" - cmd = to_bytes(cmd, errors='surrogate_or_strict') - self._session.stdin.write(cmd) - - def _wrap_command(self, cmd, sudoable, mark_start, mark_end): - ''' wrap command so stdout and status can be extracted ''' - - if self.is_windows: - if not cmd.startswith(" ".join(_common_args) + " -EncodedCommand"): - cmd = self._shell._encode_script(cmd, preserve_rc=True) - cmd = cmd + "; echo " + mark_start + "\necho " + mark_end + "\n" - else: - if sudoable: - cmd = "sudo " + cmd - cmd = "echo " + mark_start + "\n" + cmd + "\necho $'\\n'$?\n" + "echo " + mark_end + "\n" - - display.vvvv(u"_wrap_command: '{0}'".format(to_text(cmd)), host=self.host) - return cmd - - def _post_process(self, stdout, mark_begin): - ''' extract command status and strip unwanted lines ''' - - if self.is_windows: - # Value of $LASTEXITCODE will be the line after the mark - trailer = stdout[stdout.rfind(mark_begin):] - last_exit_code = trailer.splitlines()[1] - if last_exit_code.isdigit: - returncode = int(last_exit_code) - else: - returncode = -1 - # output to keep will be before the mark - stdout = stdout[:stdout.rfind(mark_begin)] - - # If it looks like JSON remove any newlines - if stdout.startswith('{'): - stdout = stdout.replace('\n', '') - - return (returncode, stdout) - else: - # Get command return code - returncode = int(stdout.splitlines()[-2]) - - # Throw away ending lines - for x in range(0, 3): - stdout = stdout[:stdout.rfind('\n')] - - return (returncode, stdout) - - def _filter_ansi(self, line): - ''' remove any ANSI terminal control codes ''' - line = to_text(line) - - if self.is_windows: - osc_filter = re.compile(r'\x1b\][^\x07]*\x07') - line = osc_filter.sub('', line) - ansi_filter = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') - line = ansi_filter.sub('', line) - - # Replace or strip sequence (at terminal width) - line = line.replace('\r\r\n', '\n') - if len(line) == 201: - line = line[:-1] - - return line - - def _flush_stderr(self, subprocess): - ''' read and return stderr with minimal blocking ''' - - poll_stderr = select.poll() - poll_stderr.register(subprocess.stderr, select.POLLIN) - stderr = '' - - while subprocess.poll() is None: - if poll_stderr.poll(1): - line = subprocess.stderr.readline() - display.vvvv(u"stderr line: {0}".format(to_text(line)), host=self.host) - stderr = stderr + line - else: - break - - return stderr - - def _get_url(self, client_method, bucket_name, out_path, http_method): - ''' Generate URL for get_object / put_object ''' - client = boto3.client('s3') - return client.generate_presigned_url(client_method, Params={'Bucket': bucket_name, 'Key': out_path}, ExpiresIn=3600, HttpMethod=http_method) - - @_ssm_retry - def _file_transport_command(self, in_path, out_path, ssm_action): - ''' transfer a file from using an intermediate S3 bucket ''' - - s3_path = out_path.replace('\\', '/') - bucket_url = 's3://%s/%s' % (self.get_option('bucket_name'), s3_path) - - if self.is_windows: - put_command = "Invoke-WebRequest -Method PUT -InFile '%s' -Uri '%s' -UseBasicParsing" % ( - in_path, self._get_url('put_object', self.get_option('bucket_name'), s3_path, 'PUT')) - get_command = "Invoke-WebRequest '%s' -OutFile '%s'" % ( - self._get_url('get_object', self.get_option('bucket_name'), s3_path, 'GET'), out_path) - else: - put_command = "curl --request PUT --upload-file '%s' '%s'" % ( - in_path, self._get_url('put_object', self.get_option('bucket_name'), s3_path, 'PUT')) - get_command = "curl '%s' -o '%s'" % ( - self._get_url('get_object', self.get_option('bucket_name'), s3_path, 'GET'), out_path) - - client = boto3.client('s3') - if ssm_action == 'get': - (returncode, stdout, stderr) = self.exec_command(put_command, in_data=None, sudoable=False) - with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb') as data: - client.download_fileobj(self.get_option('bucket_name'), s3_path, data) - else: - with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as data: - client.upload_fileobj(data, self.get_option('bucket_name'), s3_path) - (returncode, stdout, stderr) = self.exec_command(get_command, in_data=None, sudoable=False) - - # Check the return code - if returncode == 0: - return (returncode, stdout, stderr) - else: - raise AnsibleError("failed to transfer file to %s %s:\n%s\n%s" % - (to_native(in_path), to_native(out_path), to_native(stdout), to_native(stderr))) - - def put_file(self, in_path, out_path): - ''' transfer a file from local to remote ''' - - super(Connection, self).put_file(in_path, out_path) - - display.vvv(u"PUT {0} TO {1}".format(in_path, out_path), host=self.host) - if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')): - raise AnsibleFileNotFound("file or module does not exist: {0}".format(to_native(in_path))) - - return self._file_transport_command(in_path, out_path, 'put') - - def fetch_file(self, in_path, out_path): - ''' fetch a file from remote to local ''' - - super(Connection, self).fetch_file(in_path, out_path) - - display.vvv(u"FETCH {0} TO {1}".format(in_path, out_path), host=self.host) - return self._file_transport_command(in_path, out_path, 'get') - - def close(self): - ''' terminate the connection ''' - if self._session_id: - - display.vvv(u"CLOSING SSM CONNECTION TO: {0}".format(self.instance_id), host=self.host) - if self._timeout: - self._session.terminate() - else: - cmd = b"\nexit\n" - self._session.communicate(cmd) - - display.vvvv(u"TERMINATE SSM SESSION: {0}".format(self._session_id), host=self.host) - self._client.terminate_session(SessionId=self._session_id) - self._session_id = '' |