From 12b507b9a3dc4f8c5e572f22481ca655247c8717 Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Wed, 31 Jan 2024 09:56:11 -0500 Subject: [PATCH 1/4] add transit_gateway_route module Signed-off-by: Paul Czarkowski --- plugins/modules/transit_gateway_route.py | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 plugins/modules/transit_gateway_route.py diff --git a/plugins/modules/transit_gateway_route.py b/plugins/modules/transit_gateway_route.py new file mode 100644 index 00000000000..783fac8edaa --- /dev/null +++ b/plugins/modules/transit_gateway_route.py @@ -0,0 +1,192 @@ +#!/usr/bin/python + +# Copyright: (c) 2024, Paul Czarkowski +# 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 = r''' +--- +module: transit_gateway_route + +short_description: Creates a transit gateway route + +# If this is part of a collection, you need to use semantic versioning, +# i.e. the version is of the form "2.5.0" and not "2.4". +version_added: "1.0.0" + +description: | + Creates a transit gateway route to a specified transit gateway route table. + +options: + destination_cidr_block: + description: The CIDR range used for destination matches. Routing decisions are based on the most specific match. + required: true + type: str + region: + description: The AWS region to use. + required: true + type: str + transit_gateway_route_table_id: + description: The ID of the transit gateway route table. + required: true + type: str + transit_gateway_attachment_id: + description: The ID of the transit gateway attachment. + required: true + type: str + blackhole: + description: Indicates whether to drop traffic that matches this route (blackhole). Defaults to false. + required: false + type: bool + tags: + description: AWS tags + required: false + type: list + state: + description: present or absent + required: true +# Specify this value according to your collection +# in format of namespace.collection.doc_fragment_name +extends_documentation_fragment: + - amazon.aws.common.modules + +author: + - Paul Czarkowski (@paulczar) +''' + +EXAMPLES = r''' +# Create a transit gateway route +- name: Create a transit gateway route + my_namespace.my_collection.transit_gateway_route: + destination_cidr_block: 0.0.0.0/0 + region: us-east-1 + transit_gateway_route_table_id: tgw-rtb-1234567890 + transit_gateway_attachment_id: tgw-attach-1234567890 + blackhole: false + state: present +''' + +RETURN = r''' +# These are examples of possible return values, and in general should use other names for return values. +routes: + - destination_cidr_block: 0.0.0.0/0 + region: us-east-1 + transit_gateway_route_table_id: tgw-rtb-1234567890 + transit_gateway_attachment_id: tgw-attach-1234567890 +''' + +try: + from botocore.exceptions import BotoCoreError + from botocore.exceptions import ClientError +except ImportError: + pass # Handled by AnsibleAWSModule +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule +from re import sub + +def snake_case(s): + return '_'.join( + sub('([A-Z][a-z]+)', r' \1', + sub('([A-Z]+)', r' \1', + s.replace('-', ' '))).split()).lower() + +def process_response(response_in): + if not response_in: + return response_in + return camel_dict_to_snake_dict(response_in) + +def get_tgw_rt(connection,tgw_rt_id, tgw_att_id): + filters = [dict( + Name = 'attachment.transit-gateway-attachment-id', + Values = [tgw_att_id] + )] + try: + response = connection.search_transit_gateway_routes( + TransitGatewayRouteTableId=tgw_rt_id, Filters=filters, MaxResults=5) + except (BotoCoreError, ClientError) as e: + return None, e + tgw = response['Routes'] + return tgw, None + +def run_module(): + module_args = dict( + destination_cidr_block=dict(type='str', required=True), + region=dict(type='str', required=True), + transit_gateway_route_table_id=dict(type='str', required=True), + transit_gateway_attachment_id=dict(type='str', required=True), + blackhole=dict(type='bool', default=False, required=False), + # Todo: support dry run + state=dict(type='str', default='present', choices=['present','absent']), + ) + + result = dict( + changed=False, + routes=[dict()], + ) + + module = AnsibleAWSModule( + argument_spec=module_args, + supports_check_mode=True + ) + + if module.check_mode: + module.exit_json(**result) + + connection = module.client("ec2", + retry_decorator=AWSRetry.jittered_backoff(), + region=module.params['region']) + + # check to see if it exists + result['routes'], err = get_tgw_rt( + connection, + module.params['transit_gateway_route_table_id'], + module.params['transit_gateway_attachment_id']) + if err: + module.fail_json_aws(err, msg="Failed to check for existing transit gateway route") + + # if it is to be deleted + if module.params['state'] == "absent": + if not result['routes']: + module.exit_json(**result) + try: + _ = connection.delete_transit_gateway_route( + DestinationCidrBlock=module.params['destination_cidr_block'], + TransitGatewayRouteTableId=module.params['transit_gateway_route_table_id'], + # todo DryRun=module.params['string'], + ) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Failed to delete transit gateway route") + result['changed'] = True + module.exit_json(**result) + + if result['routes']: + module.exit_json(**result) + + # create it + try: + response = connection.create_transit_gateway_route( + DestinationCidrBlock=module.params['destination_cidr_block'], + TransitGatewayRouteTableId=module.params['transit_gateway_route_table_id'], + TransitGatewayAttachmentId=module.params['transit_gateway_attachment_id'], + Blackhole=module.params['blackhole'], + # todo DryRun=module.params['string'], + ) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unknown error") + + result['routes'] = [process_response(response)] + result['changed'] = True + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() From 4b348771027c623194423a01f004d8b302a06b52 Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Fri, 2 Feb 2024 15:09:34 -0500 Subject: [PATCH 2/4] attempt to fix return to pass tests Signed-off-by: Paul Czarkowski --- plugins/modules/transit_gateway_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/transit_gateway_route.py b/plugins/modules/transit_gateway_route.py index 783fac8edaa..3d3c8f44c3c 100644 --- a/plugins/modules/transit_gateway_route.py +++ b/plugins/modules/transit_gateway_route.py @@ -124,7 +124,7 @@ def run_module(): result = dict( changed=False, - routes=[dict()], + routes=[], ) module = AnsibleAWSModule( From 73a658d338bf1649b1cbd7105c22f6b33f4c8e57 Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Fri, 2 Feb 2024 15:19:31 -0500 Subject: [PATCH 3/4] attempt to fix return to pass tests Signed-off-by: Paul Czarkowski --- plugins/modules/transit_gateway_route.py | 29 +++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/plugins/modules/transit_gateway_route.py b/plugins/modules/transit_gateway_route.py index 3d3c8f44c3c..7ddf8693b38 100644 --- a/plugins/modules/transit_gateway_route.py +++ b/plugins/modules/transit_gateway_route.py @@ -122,25 +122,22 @@ def run_module(): state=dict(type='str', default='present', choices=['present','absent']), ) - result = dict( - changed=False, - routes=[], - ) - module = AnsibleAWSModule( argument_spec=module_args, supports_check_mode=True ) + changed = False + if module.check_mode: - module.exit_json(**result) + module.exit_json(changed=changed, routes=[]) connection = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff(), region=module.params['region']) # check to see if it exists - result['routes'], err = get_tgw_rt( + response, err = get_tgw_rt( connection, module.params['transit_gateway_route_table_id'], module.params['transit_gateway_attachment_id']) @@ -149,8 +146,8 @@ def run_module(): # if it is to be deleted if module.params['state'] == "absent": - if not result['routes']: - module.exit_json(**result) + if not response: + module.exit_json(changed=changed, routes=[]) try: _ = connection.delete_transit_gateway_route( DestinationCidrBlock=module.params['destination_cidr_block'], @@ -159,11 +156,11 @@ def run_module(): ) except (BotoCoreError, ClientError) as e: module.fail_json_aws(e, msg="Failed to delete transit gateway route") - result['changed'] = True - module.exit_json(**result) + changed = True + module.exit_json(changed=changed, routes=[]) - if result['routes']: - module.exit_json(**result) + if response: + module.exit_json(changed=changed, routes=process_response(response)) # create it try: @@ -177,11 +174,11 @@ def run_module(): except (BotoCoreError, ClientError) as e: module.fail_json_aws(e, msg="Unknown error") - result['routes'] = [process_response(response)] - result['changed'] = True + routes = [process_response(response)] + changed = True # in the event of a successful module execution, you will want to # simple AnsibleModule.exit_json(), passing the key/value results - module.exit_json(**result) + module.exit_json(changed=changed, routes=routes) def main(): From d401c9affa844112f85b39798ef8b91b9373c8c3 Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Fri, 2 Feb 2024 16:37:47 -0500 Subject: [PATCH 4/4] fix output Signed-off-by: Paul Czarkowski --- plugins/modules/transit_gateway_route.py | 28 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/modules/transit_gateway_route.py b/plugins/modules/transit_gateway_route.py index 7ddf8693b38..0013ab4f7ef 100644 --- a/plugins/modules/transit_gateway_route.py +++ b/plugins/modules/transit_gateway_route.py @@ -70,10 +70,30 @@ RETURN = r''' # These are examples of possible return values, and in general should use other names for return values. routes: - - destination_cidr_block: 0.0.0.0/0 - region: us-east-1 - transit_gateway_route_table_id: tgw-rtb-1234567890 - transit_gateway_attachment_id: tgw-attach-1234567890 + description: transit gateway routes + type: list + returned: always + contains: + destination_cidr_block: + description: The CIDR range used for destination matches. Routing decisions are based on the most specific match. + type: str + example: '10.0.0.0/8' + region: + description: The AWS region + type: str + example: 'us-east-1' + transit_gateway_route_table_id: + description: The ID of the transit gateway route table. + type: str + example: 'tgw-rtb-1234567890' + transit_gateway_attachment_id: + description: The ID of the transit gateway attachment. + type: str + example: 'tgw-attach-1234567890' + blackhole: + description: Indicates whether to drop traffic that matches this route (blackhole). + type: bool + example: false ''' try: