diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py new file mode 100644 index 00000000000..ee53d7bb138 --- /dev/null +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -0,0 +1,1085 @@ +#!/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 . + +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. + required: false + default: "present" + choices: ["present", "absent"] + nat_gateway_id: + description: + - The id AWS dynamically allocates to the NAT Gateway on creation. + This is required when the absent option is present. + required: false + default: None + subnet_id: + description: + - The id of the subnet to create the NAT Gateway in. This is required + with the present option. + required: false + default: None + 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. + required: false + default: None + 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. + required: false + 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 + 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. + required: false + default: true + wait: + description: + - Wait for operation to complete before returning. + required: false + default: false + wait_timeout: + description: + - How many seconds to wait for an operation to complete before timing out. + required: false + default: 300 + 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. + required: false + +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: yes + 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: yes + 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: yes + 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: yes + nat_gateway_id: "{{ item.NatGatewayId }}" + release_eip: yes + register: delete_nat_gateway_result + with_items: "{{ 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: yes + 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: yes + wait: yes + wait_timeout: 300 + region: ap-southeast-2 +''' + +RETURN = ''' +create_time: + description: The ISO 8601 date time formatin UTC. + returned: In all cases. + type: string + sample: "2016-03-05T05:19:20.282000+00:00'" +nat_gateway_id: + description: id of the VPC NAT Gateway + returned: In all cases. + type: string + sample: "nat-0d1e3a878585988f8" +subnet_id: + description: id of the Subnet + returned: In all cases. + type: string + sample: "subnet-12345" +state: + description: The current state of the NAT Gateway. + returned: In all cases. + type: string + sample: "available" +vpc_id: + description: id of the VPC. + returned: In all cases. + type: string + sample: "vpc-12345" +nat_gateway_addresses: + description: List of dictionairies containing the public_ip, network_interface_id, private_ip, and allocation_id. + returned: In all cases. + type: string + sample: [ + { + 'public_ip': '52.52.52.52', + 'network_interface_id': 'eni-12345', + 'private_ip': '10.0.0.100', + 'allocation_id': 'eipalloc-12345' + } + ] +''' + +try: + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +import datetime +import random +import re +import time + +from dateutil.tz import tzutc + +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_GATEWAY_UNCONVERTED = [ + { + 'VpcId': 'vpc-12345678', + 'State': 'available', + 'NatGatewayId': 'nat-123456789', + 'SubnetId': 'subnet-123456789', + 'NatGatewayAddresses': [ + { + 'PublicIp': '55.55.55.55', + 'NetworkInterfaceId': 'eni-1234567', + 'AllocationId': 'eipalloc-1234567', + 'PrivateIp': '10.0.0.102' + } + ], + 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) + } +] + +DRY_RUN_ALLOCATION_UNCONVERTED = { + 'Addresses': [ + { + 'PublicIp': '55.55.55.55', + 'Domain': 'vpc', + 'AllocationId': 'eipalloc-1234567' + } + ] +} + +DRY_RUN_MSGS = 'DryRun Mode:' + + +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 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(convert_to_lower(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, _, 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 + params = { + 'AllocationId': allocation_id, + } + try: + client.release_address(**params) + 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 = client.create_nat_gateway(**params)["NatGateway"] + else: + result = DRY_RUN_GATEWAY_UNCONVERTED[0] + result['CreateTime'] = datetime.datetime.utcnow() + result['NatGatewayAddresses'][0]['AllocationId'] = allocation_id + result['SubnetId'] = subnet_id + + success = True + changed = True + create_time = result['CreateTime'].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['NatGatewayId'], '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' + ) + 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, _, 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 successfull' + .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'] + ] + ) + + # 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': + if not subnet_id: + module.fail_json(msg='subnet_id is required for creation') + + 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: + if not nat_gateway_id: + module.fail_json(msg='nat_gateway_id is required for removal') + + 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 + ) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/test/integrations/group_vars/all.yml b/test/integrations/group_vars/all.yml new file mode 100644 index 00000000000..8a3ccba7168 --- /dev/null +++ b/test/integrations/group_vars/all.yml @@ -0,0 +1 @@ +test_subnet_id: 'subnet-123456789' diff --git a/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml b/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml new file mode 100644 index 00000000000..f5ad5f50fc8 --- /dev/null +++ b/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml @@ -0,0 +1,76 @@ +- name: Launching NAT Gateway and allocate a new eip. + ec2_vpc_nat_gateway: + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat + +- debug: + var: nat +- fail: + msg: "Failed to create" + when: '"{{ nat["changed"] }}" != "True"' + +- name: Launch a new gateway only if one does not exist already in this subnet. + ec2_vpc_nat_gateway: + if_exist_do_not_create: yes + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat_idempotent + +- debug: + var: nat_idempotent +- fail: + msg: "Failed to be idempotent" + when: '"{{ nat_idempotent["changed"] }}" == "True"' + +- name: Launching NAT Gateway and allocate a new eip even if one already exists in the subnet. + ec2_vpc_nat_gateway: + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: new_nat + +- debug: + var: new_nat +- fail: + msg: "Failed to create" + when: '"{{ new_nat["changed"] }}" != "True"' + +- name: Launching NAT Gateway with allocation id, this call is idempotent and will not create anything. + ec2_vpc_nat_gateway: + allocation_id: eipalloc-1234567 + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat_with_eipalloc + +- debug: + var: nat_with_eipalloc +- fail: + msg: 'Failed to be idempotent.' + when: '"{{ nat_with_eipalloc["changed"] }}" == "True"' + +- name: Delete the 1st nat gateway and do not wait for it to finish + ec2_vpc_nat_gateway: + region: us-west-2 + nat_gateway_id: "{{ nat.nat_gateway_id }}" + state: absent + +- name: Delete the nat_with_eipalloc and release the eip + ec2_vpc_nat_gateway: + region: us-west-2 + nat_gateway_id: "{{ new_nat.nat_gateway_id }}" + release_eip: yes + state: absent + wait: yes + wait_timeout: 600 diff --git a/test/integrations/site.yml b/test/integrations/site.yml new file mode 100644 index 00000000000..62416726ebc --- /dev/null +++ b/test/integrations/site.yml @@ -0,0 +1,3 @@ +- hosts: 127.0.0.1 + roles: + - { role: ec2_vpc_nat_gateway } diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py new file mode 100644 index 00000000000..1b75c88a143 --- /dev/null +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -0,0 +1,486 @@ +#!/usr/bin/python + +import boto3 +import unittest + +from collections import namedtuple +from ansible.parsing.dataloader import DataLoader +from ansible.vars import VariableManager +from ansible.inventory import Inventory +from ansible.playbook.play import Play +from ansible.executor.task_queue_manager import TaskQueueManager + +import cloud.amazon.ec2_vpc_nat_gateway as ng + +Options = ( + namedtuple( + 'Options', [ + 'connection', 'module_path', 'forks', 'become', 'become_method', + 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', + 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', + 'check' + ] + ) +) +# initialize needed objects +variable_manager = VariableManager() +loader = DataLoader() +options = ( + Options( + connection='local', + module_path='cloud/amazon', + forks=1, become=None, become_method=None, become_user=None, check=True, + remote_user=None, private_key_file=None, ssh_common_args=None, + sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, + verbosity=3 + ) +) +passwords = dict(vault_pass='') + +aws_region = 'us-west-2' + +# create inventory and pass to var manager +inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') +variable_manager.set_inventory(inventory) + +def run(play): + tqm = None + results = None + try: + tqm = TaskQueueManager( + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + options=options, + passwords=passwords, + stdout_callback='default', + ) + results = tqm.run(play) + finally: + if tqm is not None: + tqm.cleanup() + return tqm, results + +class AnsibleVpcNatGatewayTasks(unittest.TestCase): + + def test_create_gateway_using_allocation_id(self): + play_source = dict( + name = "Create new nat gateway with eip allocation-id", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-12345678', + allocation_id='eipalloc-12345678', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.failUnless(tqm._stats.changed['localhost'] == 1) + + def test_create_gateway_using_allocation_id_idempotent(self): + play_source = dict( + name = "Create new nat gateway with eip allocation-id", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-123456789', + allocation_id='eipalloc-1234567', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_create_gateway_using_eip_address(self): + play_source = dict( + name = "Create new nat gateway with eip address", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-12345678', + eip_address='55.55.55.55', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.failUnless(tqm._stats.changed['localhost'] == 1) + + def test_create_gateway_using_eip_address_idempotent(self): + play_source = dict( + name = "Create new nat gateway with eip address", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-123456789', + eip_address='55.55.55.55', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_create_gateway_in_subnet_only_if_one_does_not_exist_already(self): + play_source = dict( + name = "Create new nat gateway only if one does not exist already", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + if_exist_do_not_create='yes', + subnet_id='subnet-123456789', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_delete_gateway(self): + play_source = dict( + name = "Delete Nat Gateway", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + nat_gateway_id='nat-123456789', + state='absent', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertTrue(tqm._stats.changed.has_key('localhost')) + +class AnsibleEc2VpcNatGatewayFunctions(unittest.TestCase): + + def test_convert_to_lower(self): + example = ng.DRY_RUN_GATEWAY_UNCONVERTED + converted_example = ng.convert_to_lower(example[0]) + keys = converted_example.keys() + keys.sort() + for i in range(len(keys)): + if i == 0: + self.assertEqual(keys[i], 'create_time') + if i == 1: + self.assertEqual(keys[i], 'nat_gateway_addresses') + gw_addresses_keys = converted_example[keys[i]][0].keys() + gw_addresses_keys.sort() + for j in range(len(gw_addresses_keys)): + if j == 0: + self.assertEqual(gw_addresses_keys[j], 'allocation_id') + if j == 1: + self.assertEqual(gw_addresses_keys[j], 'network_interface_id') + if j == 2: + self.assertEqual(gw_addresses_keys[j], 'private_ip') + if j == 3: + self.assertEqual(gw_addresses_keys[j], 'public_ip') + if i == 2: + self.assertEqual(keys[i], 'nat_gateway_id') + if i == 3: + self.assertEqual(keys[i], 'state') + if i == 4: + self.assertEqual(keys[i], 'subnet_id') + if i == 5: + self.assertEqual(keys[i], 'vpc_id') + + def test_get_nat_gateways(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, stream = ( + ng.get_nat_gateways(client, 'subnet-123456789', check_mode=True) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_get_nat_gateways_no_gateways_found(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, stream = ( + ng.get_nat_gateways(client, 'subnet-1234567', check_mode=True) + ) + self.assertTrue(success) + self.assertEqual(stream, []) + + def test_wait_for_status(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, gws = ( + ng.wait_for_status( + client, 5, 'nat-123456789', 'available', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS[0] + self.assertTrue(success) + self.assertEqual(gws, should_return) + + def test_wait_for_status_to_timeout(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, gws = ( + ng.wait_for_status( + client, 2, 'nat-12345678', 'available', check_mode=True + ) + ) + self.assertFalse(success) + self.assertEqual(gws, {}) + + def test_gateway_in_subnet_exists_with_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', 'eipalloc-1234567', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertEqual(gws, should_return) + + def test_gateway_in_subnet_exists_with_allocation_id_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', 'eipalloc-123', check_mode=True + ) + ) + should_return = list() + self.assertEqual(gws, should_return) + + def test_gateway_in_subnet_exists_without_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertEqual(gws, should_return) + + def test_get_eip_allocation_id_by_address(self): + client = boto3.client('ec2', region_name=aws_region) + allocation_id, _ = ( + ng.get_eip_allocation_id_by_address( + client, '55.55.55.55', check_mode=True + ) + ) + should_return = 'eipalloc-1234567' + self.assertEqual(allocation_id, should_return) + + def test_get_eip_allocation_id_by_address_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + allocation_id, err_msg = ( + ng.get_eip_allocation_id_by_address( + client, '52.52.52.52', check_mode=True + ) + ) + self.assertEqual(err_msg, 'EIP 52.52.52.52 does not exist') + self.assertIsNone(allocation_id) + + def test_allocate_eip_address(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, eip_id = ( + ng.allocate_eip_address( + client, check_mode=True + ) + ) + self.assertTrue(success) + + def test_release_address(self): + client = boto3.client('ec2', region_name=aws_region) + success, _ = ( + ng.release_address( + client, 'eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + + def test_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.create( + client, 'subnet-123456', 'eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_pre_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_pre_create_idemptotent_with_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', allocation_id='eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_pre_create_idemptotent_with_eip_address(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', eip_address='55.55.55.55', check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_pre_create_idemptotent_if_exist_do_not_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', if_exist_do_not_create=True, check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_delete(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-123456789', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_delete_and_release_ip(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-123456789', release_eip=True, check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_delete_if_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-12345', check_mode=True + ) + ) + self.assertFalse(success) + self.assertFalse(changed) + +def main(): + unittest.main() + +if __name__ == '__main__': + main()