From f86c5f84657c4da2729df5792a8c515f0f4c7f47 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 21 Mar 2016 06:07:04 -0700 Subject: [PATCH 01/13] Manage AWS Nat Gateways * Create an AWS Nat Gateway. * Delete an AWS Nat Gateway. * If Nat Gateway exist in subnet and the option is passed to not create one, it will then return the Nat Gateway object. --- cloud/amazon/ec2_vpc_nat_gateway.py | 930 ++++++++++++++++++++++++++++ 1 file changed, 930 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_nat_gateway.py diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py new file mode 100644 index 00000000000..9b1fb235a61 --- /dev/null +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -0,0 +1,930 @@ +#!/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.1" +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 elasti 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. + required: false + default: true + wait: + description: + - Wait for operation to complete before returning + required: false + default: true + 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 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 with when condition + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + eip_address: 52.1.1.1 + region: ap-southeast-2 + register: new_nat_gateway + when: existing_nat_gateways.result == [] + +- name: Create new nat gateway 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 + 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 time +import datetime + +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.. NatGatewayAddresses == nat_gateway_addresses + if a val is of type datetime.datetime, it will be converted to + the ISO 8601 + + Basic Usage: + >>> test = {'NatGatewaysAddresses': []} + >>> test = convert_to_lower(test) + { + 'nat_gateways_addresses': [] + } + + Returns: + Dictionary + """ + results = dict() + for key, val in data.items(): + key = re.sub('([A-Z]{1})', r'_\1', key).lower()[1:] + if isinstance(val, datetime.datetime): + results[key] = val.isoformat() + else: + results[key] = val + return results + +def formatted_nat_gw_output(data): + """Format the results of NatGateways into lowercase with underscores. + Args: + data (list): List of dictionaries with keys that have upper cases in + them. Example.. NatGatewayAddresses == nat_gateway_addresses + if a val is of type datetime.datetime, it will be converted to + the ISO 8601 + + Basic Usage: + >>> test = [ + { + 'VpcId': 'vpc-12345', + 'State': 'available', + 'NatGatewayId': 'nat-0b2f9f2ac3f51a653', + 'SubnetId': 'subnet-12345', + 'NatGatewayAddresses': [ + { + 'PublicIp': '52.52.52.52', + 'NetworkInterfaceId': 'eni-12345', + 'AllocationId': 'eipalloc-12345', + 'PrivateIp': '10.0.0.100' + } + ], + 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) + } + ] + >>> test = formatted_nat_gw_output(test) + [ + { + 'nat_gateway_id': 'nat-0b2f9f2ac3f51a653', + 'subnet_id': 'subnet-12345', + 'nat_gateway_addresses': [ + { + 'public_ip': '52.52.52.52', + 'network_interface_id': 'eni-12345', + 'private_ip': '10.0.0.100', + 'allocation_id': 'eipalloc-12345' + } + ], + 'state': 'available', + 'create_time': '2016-03-05T05:19:20.282000+00:00', + 'vpc_id': 'vpc-12345' + } + ] + + Returns: + List + """ + results = list() + for gw in data: + output = dict() + ng_addresses = gw.pop('NatGatewayAddresses') + output = convert_to_lower(gw) + output['nat_gateway_addresses'] = [] + for address in ng_addresses: + gw_data = convert_to_lower(address) + output['nat_gateway_addresses'].append(gw_data) + results.append(output) + + return results + +def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): + """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. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> get_nat_gateways(client, subnet_id) + [ + 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 = dict() + err_msg = "" + gateways_retrieved = False + if nat_gateway_id: + params['NatGatewayIds'] = [nat_gateway_id] + else: + params['Filter'] = [ + { + 'Name': 'subnet-id', + 'Values': [subnet_id] + } + ] + + try: + gateways = client.describe_nat_gateways(**params)['NatGateways'] + existing_gateways = formatted_nat_gw_output(gateways) + gateways_retrieved = True + 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): + """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-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> wait_for_status(client, subnet_id, allocation_id) + [ + 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) + """ + polling_increment_secs = 5 + wait_timeout = time.time() + wait_timeout + status_achieved = False + nat_gateway = list() + err_msg = "" + + while wait_timeout > time.time(): + try: + gws_retrieved, err_msg, nat_gateway = ( + get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + ) + if gws_retrieved and nat_gateway: + if nat_gateway[0].get('state') == status: + status_achieved = True + break + + elif nat_gateway[0].get('state') == 'failed': + err_msg = nat_gateway[0].get('failure_message') + 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): + """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-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> gateway_in_subnet_exists(client, subnet_id, allocation_id) + ( + [ + { + "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" + } + ], + False + ) + + Returns: + Tuple (list, bool) + """ + allocation_id_exists = False + gateways = [] + gws_retrieved, _, gws = get_nat_gateways(client, subnet_id) + if not gws_retrieved: + return gateways, allocation_id_exists + for gw in gws: + for address in gw['nat_gateway_addresses']: + if gw.get('state') == 'available' or gw.get('state') == 'pending': + 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 = "" + if check_mode: + return "eipalloc-123456", err_msg + try: + allocation = client.describe_addresses(**params)['Addresses'][0] + if not allocation.get('Domain') != 'vpc': + err_msg = ( + "EIP provided is a non-VPC EIP, please allocate a VPC scoped EIP" + ) + else: + allocation_id = allocation.get('AllocationId') + 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) + """ + if check_mode: + return True, "eipalloc-123456" + + ip_allocated = False + params = { + 'Domain': 'vpc' + } + try: + new_eip = client.allocate_address(**params) + ip_allocated = True + except botocore.exceptions.ClientError: + pass + + return ip_allocated, new_eip['AllocationId'] + +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 + """ + if check_mode: + return True + + ip_released = False + params = { + 'AllocationId': allocation_id, + } + try: + client.release_address(**params) + ip_released = True + except botocore.exceptions.ClientError: + pass + + return ip_released + +def create(client, subnet_id, allocation_id, client_token=None, + wait=False, wait_timeout=0, if_exist_do_not_create=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-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> 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, str, list) + """ + params = { + 'SubnetId': subnet_id, + 'AllocationId': allocation_id + } + request_time = datetime.datetime.utcnow() + changed = False + token_provided = False + err_msg = "" + + if client_token: + token_provided = True + params['ClientToken'] = client_token + + try: + result = client.create_nat_gateway(**params)["NatGateway"] + changed = True + create_time = result['CreateTime'].replace(tzinfo=None) + if token_provided and (request_time > create_time): + changed = False + elif wait: + status_achieved, err_msg, result = ( + wait_for_status( + client, wait_timeout, result['NatGatewayId'], 'available' + ) + ) + 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) + + return 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): + """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, str, list) + """ + 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) + ) + if len(existing_gateways) > 0 and if_exist_do_not_create: + results = existing_gateways + return changed, err_msg, results + else: + _, allocation_id = allocate_eip_address(client) + + elif eip_address or allocation_id: + if eip_address and not allocation_id: + allocation_id = get_eip_allocation_id_by_address(client) + + existing_gateways, allocation_id_exists = ( + gateway_in_subnet_exists(client, subnet_id, allocation_id) + ) + if len(existing_gateways) > 0 and (allocation_id_exists or if_exist_do_not_create): + results = existing_gateways + return changed, err_msg, results + + changed, err_msg, results = create( + client, subnet_id, allocation_id, client_token, + wait, wait_timeout,if_exist_do_not_create + ) + + return changed, err_msg, results + +def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=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 + } + changed = False + err_msg = "" + results = list() + try: + exist, _, gw = get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + if exist and len(gw) == 1: + results = gw[0] + result = client.delete_nat_gateway(**params) + result = convert_to_lower(result) + allocation_id = ( + results['nat_gateway_addresses'][0]['allocation_id'] + ) + changed = True + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + if wait and not err_msg: + status_achieved, err_msg, results = ( + wait_for_status(client, wait_timeout, nat_gateway_id, 'deleted') + ) + + if release_eip: + eip_released = release_address(client, allocation_id) + if not eip_released: + err_msg = "Failed to release eip %s".format(allocation_id) + + return changed, err_msg, results + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + subnet_id=dict(), + eip_address=dict(), + allocation_id=dict(), + if_exist_do_not_create=dict(type='bool', default=False), + state=dict(default='present', choices=['present', 'absent']), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=320, required=False), + release_eip=dict(type='bool', default=False), + nat_gateway_id=dict(), + client_token=dict(), + ) + ) + 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, e: + module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) + + changed = False + err_msg = None + + #Ensure resource is present + if state == 'present': + if not subnet_id: + module.fail_json(msg='subnet_id is required for creation') + + elif check_mode: + changed = True + results = 'Would have created NAT Gateway if not in check mode' + else: + changed, err_msg, results = ( + pre_create( + client, subnet_id, allocation_id, eip_address, + if_exist_do_not_create, wait, wait_timeout, + client_token + ) + ) + else: + if not nat_gateway_id: + module.fail_json(msg='nat_gateway_id is required for removal') + + elif check_mode: + changed = True + results = 'Would have deleted NAT Gateway if not in check mode' + else: + changed, err_msg, results = ( + remove(client, nat_gateway_id, wait, wait_timeout, release_eip) + ) + + if err_msg: + module.fail_json(msg=err_msg) + else: + module.exit_json(changed=changed, **results[0]) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() + From 349db85b00ec97f3274e4a4bb8c91c4422bb1c30 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 23 Mar 2016 09:00:04 -0700 Subject: [PATCH 02/13] Fixed the missing argument to get_eip_allocation_id_by_address --- cloud/amazon/ec2_vpc_nat_gateway.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 9b1fb235a61..307a9646e5e 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -681,7 +681,7 @@ def create(client, subnet_id, allocation_id, client_token=None, 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): + client_token=None, check_mode=False): """Create an Amazon NAT Gateway. Args: client (botocore.client.EC2): Boto3 client @@ -747,7 +747,11 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, elif eip_address or allocation_id: if eip_address and not allocation_id: - allocation_id = get_eip_allocation_id_by_address(client) + allocation_id = ( + get_eip_allocation_id_by_address( + client, eip_address, check_mode=check_mode + ) + ) existing_gateways, allocation_id_exists = ( gateway_in_subnet_exists(client, subnet_id, allocation_id) From ee523be26c3d4677f0393dc9976ecd1778098324 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 28 Mar 2016 20:38:52 -0700 Subject: [PATCH 03/13] Updated module to be compliant with test cases. * Added integration tests * Added unit tests --- cloud/amazon/ec2_vpc_nat_gateway.py | 553 +++++++++++------- test/integrations/group_vars/all.yml | 1 + .../roles/ec2_vpc_nat_gateway/tasks/main.yml | 76 +++ test/integrations/site.yml | 3 + .../cloud/amazon/test_ec2_vpc_nat_gateway.py | 486 +++++++++++++++ 5 files changed, 910 insertions(+), 209 deletions(-) create mode 100644 test/integrations/group_vars/all.yml create mode 100644 test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml create mode 100644 test/integrations/site.yml create mode 100644 test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 307a9646e5e..a333a9ac925 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -68,7 +68,7 @@ options: description: - Wait for operation to complete before returning required: false - default: true + default: false wait_timeout: description: - How many seconds to wait for an operation to complete before timing out @@ -94,32 +94,23 @@ 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 allocation-id ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 - allocation_id: eipalloc-12345678 + eip_address: 52.1.1.1 region: ap-southeast-2 + client_token: abcd-12345678 register: new_nat_gateway -- name: Create new nat gateway with when condition +- name: Create new nat gateway using an allocation-id ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 - eip_address: 52.1.1.1 + allocation_id: eipalloc-12345678 region: ap-southeast-2 register: new_nat_gateway - when: existing_nat_gateways.result == [] -- name: Create new nat gateway and wait for available status +- name: Create new nat gateway, using an eip address and wait for available status ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -218,98 +209,98 @@ try: except ImportError: HAS_BOTO3 = False -import time import datetime +import random +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.. NatGatewayAddresses == nat_gateway_addresses + Example.. FooBar == foo_bar if a val is of type datetime.datetime, it will be converted to the ISO 8601 Basic Usage: - >>> test = {'NatGatewaysAddresses': []} + >>> test = {'FooBar': []} >>> test = convert_to_lower(test) { - 'nat_gateways_addresses': [] + 'foo_bar': [] } Returns: Dictionary """ results = dict() - for key, val in data.items(): - key = re.sub('([A-Z]{1})', r'_\1', key).lower()[1:] - if isinstance(val, datetime.datetime): - results[key] = val.isoformat() - else: - results[key] = val - return results - -def formatted_nat_gw_output(data): - """Format the results of NatGateways into lowercase with underscores. - Args: - data (list): List of dictionaries with keys that have upper cases in - them. Example.. NatGatewayAddresses == nat_gateway_addresses - if a val is of type datetime.datetime, it will be converted to - the ISO 8601 - - Basic Usage: - >>> test = [ - { - 'VpcId': 'vpc-12345', - 'State': 'available', - 'NatGatewayId': 'nat-0b2f9f2ac3f51a653', - 'SubnetId': 'subnet-12345', - 'NatGatewayAddresses': [ - { - 'PublicIp': '52.52.52.52', - 'NetworkInterfaceId': 'eni-12345', - 'AllocationId': 'eipalloc-12345', - 'PrivateIp': '10.0.0.100' - } - ], - 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) - } - ] - >>> test = formatted_nat_gw_output(test) - [ - { - 'nat_gateway_id': 'nat-0b2f9f2ac3f51a653', - 'subnet_id': 'subnet-12345', - 'nat_gateway_addresses': [ - { - 'public_ip': '52.52.52.52', - 'network_interface_id': 'eni-12345', - 'private_ip': '10.0.0.100', - 'allocation_id': 'eipalloc-12345' - } - ], - 'state': 'available', - 'create_time': '2016-03-05T05:19:20.282000+00:00', - 'vpc_id': 'vpc-12345' - } - ] - - Returns: - List - """ - results = list() - for gw in data: - output = dict() - ng_addresses = gw.pop('NatGatewayAddresses') - output = convert_to_lower(gw) - output['nat_gateway_addresses'] = [] - for address in ng_addresses: - gw_data = convert_to_lower(address) - output['nat_gateway_addresses'].append(gw_data) - results.append(output) - + 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): +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 @@ -317,29 +308,31 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): 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-w4t12897' + >>> subnet_id = 'subnet-12345678' >>> get_nat_gateways(client, subnet_id) [ true, "", { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-123456789", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "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-w68571b5" + "vpc_id": "vpc-12345678" } Returns: @@ -348,6 +341,8 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): params = dict() err_msg = "" gateways_retrieved = False + if not states: + states = ['available', 'pending'] if nat_gateway_id: params['NatGatewayIds'] = [nat_gateway_id] else: @@ -355,19 +350,39 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): { 'Name': 'subnet-id', 'Values': [subnet_id] + }, + { + 'Name': 'state', + 'Values': states } ] try: - gateways = client.describe_nat_gateways(**params)['NatGateways'] - existing_gateways = formatted_nat_gw_output(gateways) - gateways_retrieved = True - except botocore.exceptions.ClientError as e: - err_msg = str(e) + if not check_mode: + gateways = client.describe_nat_gateways(**params)['NatGateways'] + existing_gateways = list() + if gateways: + for gw in gateways: + existing_gateways.append(convert_to_lower(gw)) + gateways_retrieved = True + else: + gateways_retrieved = True + existing_gateways = [] + 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, e: + err_msg = str(e) return gateways_retrieved, err_msg, existing_gateways -def wait_for_status(client, wait_timeout, nat_gateway_id, status): +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 @@ -378,27 +393,27 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> subnet_id = 'subnet-12345678' + >>> allocation_id = 'eipalloc-12345678' >>> wait_for_status(client, subnet_id, allocation_id) [ true, "", { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-1234567", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "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-w68571b5" + "vpc_id": "vpc-12345677" } ] @@ -409,25 +424,40 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): wait_timeout = time.time() + wait_timeout status_achieved = False nat_gateway = list() + states = ['pending', 'failed', 'available', 'deleting', 'deleted'] err_msg = "" while wait_timeout > time.time(): try: gws_retrieved, err_msg, nat_gateway = ( - get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + get_nat_gateways( + client, nat_gateway_id=nat_gateway_id, + states=states, check_mode=check_mode + ) ) if gws_retrieved and nat_gateway: - if nat_gateway[0].get('state') == status: + nat_gateway = nat_gateway[0] + if check_mode: + nat_gateway['state'] = status + + if nat_gateway.get('state') == status: status_achieved = True break - elif nat_gateway[0].get('state') == 'failed': - err_msg = nat_gateway[0].get('failure_message') + elif nat_gateway.get('state') == 'failed': + err_msg = nat_gateway.get('failure_message') break + elif nat_gateway.get('state') == 'pending': + if nat_gateway.has_key('failure_message'): + err_msg = nat_gateway.get('failure_message') + status_achieved = False + break + else: time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: + + except botocore.exceptions.ClientError, e: err_msg = str(e) if not status_achieved: @@ -435,7 +465,8 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): return status_achieved, err_msg, nat_gateway -def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): +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. @@ -446,26 +477,26 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> subnet_id = 'subnet-1234567' + >>> allocation_id = 'eipalloc-1234567' >>> gateway_in_subnet_exists(client, subnet_id, allocation_id) ( [ { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-123456789", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "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-w68571b5" + "vpc_id": "vpc-1234567" } ], False @@ -476,18 +507,22 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): """ allocation_id_exists = False gateways = [] - gws_retrieved, _, gws = get_nat_gateways(client, subnet_id) + 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 gw.get('state') == 'available' or gw.get('state') == 'pending': - if allocation_id: - if address.get('allocation_id') == allocation_id: - allocation_id_exists = True - gateways.append(gw) - else: + 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 @@ -511,22 +546,40 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): Tuple (str, str) """ params = { - 'PublicIps': [eip_address] + 'PublicIps': [eip_address], } allocation_id = None err_msg = "" - if check_mode: - return "eipalloc-123456", err_msg try: - allocation = client.describe_addresses(**params)['Addresses'][0] - if not allocation.get('Domain') != 'vpc': - err_msg = ( - "EIP provided is a non-VPC EIP, please allocate a VPC scoped EIP" + 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: - allocation_id = allocation.get('AllocationId') - except botocore.exceptions.ClientError as e: - err_msg = str(e) + err_msg = ( + "EIP {0} does not exist".format(eip_address) + ) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) return allocation_id, err_msg @@ -547,20 +600,28 @@ def allocate_eip_address(client, check_mode=False): Returns: Tuple (bool, str) """ - if check_mode: - return True, "eipalloc-123456" - ip_allocated = False + new_eip = None + err_msg = '' params = { - 'Domain': 'vpc' + 'Domain': 'vpc', } try: - new_eip = client.allocate_address(**params) - ip_allocated = True - except botocore.exceptions.ClientError: - pass + 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, e: + err_msg = str(e) - return ip_allocated, new_eip['AllocationId'] + return ip_allocated, err_msg, new_eip def release_address(client, allocation_id, check_mode=False): """Release an EIP from your EIP Pool @@ -597,7 +658,8 @@ def release_address(client, allocation_id, check_mode=False): return ip_released def create(client, subnet_id, allocation_id, client_token=None, - wait=False, wait_timeout=0, if_exist_do_not_create=False): + 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 @@ -617,27 +679,27 @@ def create(client, subnet_id, allocation_id, client_token=None, Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> 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-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-1234567", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "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-w68571b5" + "vpc_id": "vpc-1234567" } ] @@ -650,6 +712,7 @@ def create(client, subnet_id, allocation_id, client_token=None, } request_time = datetime.datetime.utcnow() changed = False + success = False token_provided = False err_msg = "" @@ -658,17 +721,34 @@ def create(client, subnet_id, allocation_id, client_token=None, params['ClientToken'] = client_token try: - result = client.create_nat_gateway(**params)["NatGateway"] + 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: - status_achieved, err_msg, result = ( + success, err_msg, result = ( wait_for_status( - client, wait_timeout, result['NatGatewayId'], 'available' + client, wait_timeout, result['NatGatewayId'], 'available', + check_mode=check_mode ) ) + if success: + err_msg = ( + 'Nat gateway {0} created'.format(result['nat_gateway_id']) + ) + if check_mode: + result['nat_gateway_addresses'][0]['allocation_id'] = allocation_id + result['subnet_id'] = subnet_id + except botocore.exceptions.ClientError as e: if "IdempotentParameterMismatch" in e.message: err_msg = ( @@ -676,8 +756,10 @@ def create(client, subnet_id, allocation_id, client_token=None, ) else: err_msg = str(e) + success = False + changed = False - return changed, err_msg, result + 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, @@ -729,45 +811,74 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, ] Returns: - Tuple (bool, str, list) + 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) + gateway_in_subnet_exists(client, subnet_id, check_mode=check_mode) ) + if len(existing_gateways) > 0 and if_exist_do_not_create: - results = existing_gateways - return changed, err_msg, results + 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: - _, allocation_id = allocate_eip_address(client) + 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 = ( + 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) + 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): - results = existing_gateways - return changed, err_msg, results + 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 - changed, err_msg, results = create( + success, changed, err_msg, results = create( client, subnet_id, allocation_id, client_token, - wait, wait_timeout,if_exist_do_not_create + wait, wait_timeout, if_exist_do_not_create, check_mode=check_mode ) - return changed, err_msg, results + return success, changed, err_msg, results -def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False): +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 @@ -809,47 +920,71 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False 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) + 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] - result = client.delete_nat_gateway(**params) - result = convert_to_lower(result) + 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 wait and not err_msg: - status_achieved, err_msg, results = ( - wait_for_status(client, wait_timeout, nat_gateway_id, 'deleted') - ) - if release_eip: - eip_released = release_address(client, allocation_id) + eip_released = ( + release_address(client, allocation_id, check_mode=check_mode) + ) if not eip_released: err_msg = "Failed to release eip %s".format(allocation_id) - return changed, err_msg, results + return success, changed, err_msg, results def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - subnet_id=dict(), - eip_address=dict(), - allocation_id=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=True), + 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(), - client_token=dict(), + nat_gateway_id=dict(type='str'), + client_token=dict(type='str'), ) ) module = AnsibleModule( @@ -890,40 +1025,40 @@ def main(): module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) changed = False - err_msg = None + err_msg = '' #Ensure resource is present if state == 'present': if not subnet_id: module.fail_json(msg='subnet_id is required for creation') - elif check_mode: - changed = True - results = 'Would have created NAT Gateway if not in check mode' - else: - changed, err_msg, results = ( - pre_create( - client, subnet_id, allocation_id, eip_address, - if_exist_do_not_create, wait, wait_timeout, - client_token - ) + 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') - elif check_mode: - changed = True - results = 'Would have deleted NAT Gateway if not in check mode' else: - changed, err_msg, results = ( - remove(client, nat_gateway_id, wait, wait_timeout, release_eip) + success, changed, err_msg, results = ( + remove( + client, nat_gateway_id, wait, wait_timeout, release_eip, + check_mode=check_mode + ) ) - if err_msg: - module.fail_json(msg=err_msg) + if not success: + module.exit_json( + msg=err_msg, success=success, changed=changed + ) else: - module.exit_json(changed=changed, **results[0]) + module.exit_json( + msg=err_msg, success=success, changed=changed, **results + ) # import module snippets from ansible.module_utils.basic import * 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..e2d3573499e --- /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() From aa189b8d98bdb29913a38e7546b336194d84979e Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Apr 2016 14:29:59 -0700 Subject: [PATCH 04/13] Added default result of None in catch statement --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index a333a9ac925..3d11e372463 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -264,6 +264,7 @@ 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 @@ -758,6 +759,7 @@ def create(client, subnet_id, allocation_id, client_token=None, err_msg = str(e) success = False changed = False + result = None return success, changed, err_msg, result From 79ea5532003ac0c1d5010020cbeb44329f24ded5 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Apr 2016 17:39:32 -0700 Subject: [PATCH 05/13] Module requires boto due to ec2.py --- cloud/amazon/ec2_vpc_nat_gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 3d11e372463..46b75ce3090 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -205,6 +205,7 @@ nat_gateway_addresses: try: import botocore import boto3 + import boto HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False From 2e42c7244762eaae2035ffca84ae90e03fb3fe96 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 6 May 2016 13:28:26 -0700 Subject: [PATCH 06/13] version bump --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 46b75ce3090..69199f90537 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -20,7 +20,7 @@ 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.1" +version_added: "2.2" requirements: [boto3, botocore] options: state: From 4e8e38f631dcb18396379abb01a30245a9f8a7ee Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 6 May 2016 13:39:36 -0700 Subject: [PATCH 07/13] remove boto --- cloud/amazon/ec2_vpc_nat_gateway.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 69199f90537..ca920c52b28 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -205,7 +205,6 @@ nat_gateway_addresses: try: import botocore import boto3 - import boto HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False From 8af106378506e53f65a94113aef965a1cce9a57c Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 25 Jul 2016 14:08:40 -0700 Subject: [PATCH 08/13] Make sure to catch if no gateways exist --- cloud/amazon/ec2_vpc_nat_gateway.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index ca920c52b28..1b574840f96 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -342,6 +342,7 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, params = dict() err_msg = "" gateways_retrieved = False + existing_gateways = list() if not states: states = ['available', 'pending'] if nat_gateway_id: @@ -361,14 +362,12 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, try: if not check_mode: gateways = client.describe_nat_gateways(**params)['NatGateways'] - existing_gateways = list() if gateways: for gw in gateways: existing_gateways.append(convert_to_lower(gw)) gateways_retrieved = True else: gateways_retrieved = True - existing_gateways = [] if nat_gateway_id: if DRY_RUN_GATEWAYS[0]['nat_gateway_id'] == nat_gateway_id: existing_gateways = DRY_RUN_GATEWAYS From ab62c644bc9f91f025b7e77480b0258d55bbb307 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 25 Jul 2016 14:19:35 -0700 Subject: [PATCH 09/13] updated catch statement to pass test (as e) --- cloud/amazon/ec2_vpc_nat_gateway.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 1b574840f96..1364aa7875f 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -376,7 +376,7 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, existing_gateways = DRY_RUN_GATEWAYS err_msg = '{0} Retrieving gateways'.format(DRY_RUN_MSGS) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return gateways_retrieved, err_msg, existing_gateways @@ -457,7 +457,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, else: time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) if not status_achieved: @@ -578,7 +578,7 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): "EIP {0} does not exist".format(eip_address) ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return allocation_id, err_msg @@ -618,7 +618,7 @@ def allocate_eip_address(client, check_mode=False): ip_allocated = True err_msg = 'eipalloc id {0} created'.format(new_eip) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return ip_allocated, err_msg, new_eip @@ -1022,7 +1022,7 @@ def main(): region=region, endpoint=ec2_url, **aws_connect_kwargs ) ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) changed = False From 123c70546e9cbe9fd2030c898d05b40f46b1129b Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 3 Aug 2016 12:50:16 -0700 Subject: [PATCH 10/13] clean up documentation --- cloud/amazon/ec2_vpc_nat_gateway.py | 86 ++++++++++++++++------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 1364aa7875f..d1d38e0e030 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' --- module: ec2_vpc_nat_gateway -short_description: Manage AWS VPC NAT Gateways +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" @@ -25,7 +25,7 @@ requirements: [boto3, botocore] options: state: description: - - Ensure NAT Gateway is present or absent + - Ensure NAT Gateway is present or absent. required: false default: "present" choices: ["present", "absent"] @@ -44,18 +44,18 @@ options: 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 + eip_address is not passed. An EIP is generated for this NAT Gateway. required: false default: None eip_address: description: - - The elasti 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 + - 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. + - if a NAT Gateway exists already in the subnet_id, then do not create a new one. required: false default: false release_eip: @@ -66,12 +66,12 @@ options: default: true wait: description: - - Wait for operation to complete before returning + - 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 + - How many seconds to wait for an operation to complete before timing out. required: false default: 300 client_token: @@ -93,7 +93,7 @@ extends_documentation_fragment: EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. -- name: Create new nat gateway with client token +- name: Create new nat gateway with client token. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -102,7 +102,7 @@ EXAMPLES = ''' client_token: abcd-12345678 register: new_nat_gateway -- name: Create new nat gateway using an allocation-id +- name: Create new nat gateway using an allocation-id. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -110,7 +110,7 @@ EXAMPLES = ''' region: ap-southeast-2 register: new_nat_gateway -- name: Create new nat gateway, using an eip address and wait for available status +- name: Create new nat gateway, using an EIP address and wait for available status. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -119,7 +119,7 @@ EXAMPLES = ''' region: ap-southeast-2 register: new_nat_gateway -- name: Create new nat gateway and allocate new eip +- name: Create new nat gateway and allocate new EIP. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -127,7 +127,7 @@ EXAMPLES = ''' 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. +- 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 @@ -136,7 +136,7 @@ EXAMPLES = ''' if_exist_do_not_create: true register: new_nat_gateway -- name: Delete nat gateway using discovered nat gateways from facts module +- name: Delete nat gateway using discovered nat gateways from facts module. ec2_vpc_nat_gateway: state: absent region: ap-southeast-2 @@ -146,7 +146,7 @@ EXAMPLES = ''' register: delete_nat_gateway_result with_items: "{{ gateways_to_remove.result }}" -- name: Delete nat gateway and wait for deleted status +- name: Delete nat gateway and wait for deleted status. ec2_vpc_nat_gateway: state: absent nat_gateway_id: nat-12345678 @@ -154,7 +154,7 @@ EXAMPLES = ''' wait_timeout: 500 region: ap-southeast-2 -- name: Delete nat gateway and release EIP +- name: Delete nat gateway and release EIP. ec2_vpc_nat_gateway: state: absent nat_gateway_id: nat-12345678 @@ -179,7 +179,7 @@ subnet_id: type: string sample: "subnet-12345" state: - description: The current state of the Nat Gateway. + description: The current state of the NAT Gateway. returned: In all cases. type: string sample: "available" @@ -211,6 +211,7 @@ except ImportError: import datetime import random +import re import time from dateutil.tz import tzutc @@ -262,6 +263,7 @@ DRY_RUN_ALLOCATION_UNCONVERTED = { DRY_RUN_MSGS = 'DryRun Mode:' + def convert_to_lower(data): """Convert all uppercase keys in dict with lowercase_ @@ -300,6 +302,7 @@ def convert_to_lower(data): 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 @@ -381,9 +384,10 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, 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 + """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. @@ -418,25 +422,25 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, ] Returns: - Tuple (bool, str, list) + Tuple (bool, str, dict) """ polling_increment_secs = 5 wait_timeout = time.time() + wait_timeout status_achieved = False - nat_gateway = list() + nat_gateway = dict() states = ['pending', 'failed', 'available', 'deleting', 'deleted'] err_msg = "" while wait_timeout > time.time(): try: - gws_retrieved, err_msg, nat_gateway = ( + 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_gateway: - nat_gateway = nat_gateway[0] + if gws_retrieved and nat_gateways: + nat_gateway = nat_gateways[0] if check_mode: nat_gateway['state'] = status @@ -449,7 +453,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, break elif nat_gateway.get('state') == 'pending': - if nat_gateway.has_key('failure_message'): + if 'failure_message' in nat_gateway: err_msg = nat_gateway.get('failure_message') status_achieved = False break @@ -465,6 +469,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, 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. @@ -472,7 +477,7 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, subnet_id (str): The subnet_id the nat resides in. Kwargs: - allocation_id (str): The eip Amazon identifier. + allocation_id (str): The EIP Amazon identifier. default = None Basic Usage: @@ -526,6 +531,7 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, 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: @@ -583,6 +589,7 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): return allocation_id, err_msg + def allocate_eip_address(client, check_mode=False): """Release an EIP from your EIP Pool Args: @@ -623,6 +630,7 @@ def allocate_eip_address(client, check_mode=False): return ip_allocated, err_msg, new_eip + def release_address(client, allocation_id, check_mode=False): """Release an EIP from your EIP Pool Args: @@ -657,6 +665,7 @@ def release_address(client, allocation_id, check_mode=False): return ip_released + def create(client, subnet_id, allocation_id, client_token=None, wait=False, wait_timeout=0, if_exist_do_not_create=False, check_mode=False): @@ -743,11 +752,8 @@ def create(client, subnet_id, allocation_id, client_token=None, ) if success: err_msg = ( - 'Nat gateway {0} created'.format(result['nat_gateway_id']) + 'NAT gateway {0} created'.format(result['nat_gateway_id']) ) - if check_mode: - result['nat_gateway_addresses'][0]['allocation_id'] = allocation_id - result['subnet_id'] = subnet_id except botocore.exceptions.ClientError as e: if "IdempotentParameterMismatch" in e.message: @@ -762,16 +768,17 @@ def create(client, subnet_id, allocation_id, client_token=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): + 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. + allocation_id (str): The EIP Amazon identifier. default = None eip_address (str): The Elastic IP Address of the EIP. default = None @@ -829,7 +836,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, changed = False results = existing_gateways[0] err_msg = ( - 'Nat Gateway {0} already exists in subnet_id {1}' + 'NAT Gateway {0} already exists in subnet_id {1}' .format( existing_gateways[0]['nat_gateway_id'], subnet_id ) @@ -864,7 +871,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, changed = False results = existing_gateways[0] err_msg = ( - 'Nat Gateway {0} already exists in subnet_id {1}' + 'NAT Gateway {0} already exists in subnet_id {1}' .format( existing_gateways[0]['nat_gateway_id'], subnet_id ) @@ -878,6 +885,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, 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. @@ -944,7 +952,7 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, changed = True success = True err_msg = ( - 'Nat gateway {0} is in a deleting state. Delete was successfull' + 'NAT gateway {0} is in a deleting state. Delete was successfull' .format(nat_gateway_id) ) @@ -957,7 +965,7 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, ) if status_achieved: err_msg = ( - 'Nat gateway {0} was deleted successfully' + 'NAT gateway {0} was deleted successfully' .format(nat_gateway_id) ) @@ -969,10 +977,11 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_address(client, allocation_id, check_mode=check_mode) ) if not eip_released: - err_msg = "Failed to release eip %s".format(allocation_id) + err_msg = "Failed to release EIP %s".format(allocation_id) return success, changed, err_msg, results + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -1067,4 +1076,3 @@ from ansible.module_utils.ec2 import * if __name__ == '__main__': main() - From 461553bda80dafc33c006f58640cf1254ae057d7 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 13:10:11 -0700 Subject: [PATCH 11/13] updated tests to reflect dict vs list --- test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py index e2d3573499e..7c4f163ad40 100644 --- a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -329,7 +329,7 @@ class AnsibleEc2VpcNatGatewayFunctions(unittest.TestCase): ) ) self.assertFalse(success) - self.assertEqual(gws, []) + self.assertEqual(gws, {}) def test_gateway_in_subnet_exists_with_allocation_id(self): client = boto3.client('ec2', region_name=aws_region) From dcf4d7e6e5d86c518df5f91cc236b23b30fcdac0 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 15:36:39 -0700 Subject: [PATCH 12/13] fail_json when error and not exit_json --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index d1d38e0e030..b5874128f3d 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -1062,7 +1062,7 @@ def main(): ) if not success: - module.exit_json( + module.fail_json( msg=err_msg, success=success, changed=changed ) else: From 950d76af0b73aff58ab0b9042ee8789b0d1f7dfb Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 18:14:36 -0700 Subject: [PATCH 13/13] fixed error message for releasing an ip when not waiting for the nat gateway to delete successfully 1st --- cloud/amazon/ec2_vpc_nat_gateway.py | 25 ++++++++++++------- .../cloud/amazon/test_ec2_vpc_nat_gateway.py | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index b5874128f3d..ee53d7bb138 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -62,6 +62,7 @@ options: 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: @@ -159,6 +160,8 @@ EXAMPLES = ''' state: absent nat_gateway_id: nat-12345678 release_eip: yes + wait: yes + wait_timeout: 300 region: ap-southeast-2 ''' @@ -648,10 +651,11 @@ def release_address(client, allocation_id, check_mode=False): True Returns: - Boolean + Boolean, string """ + err_msg = '' if check_mode: - return True + return True, '' ip_released = False params = { @@ -660,10 +664,10 @@ def release_address(client, allocation_id, check_mode=False): try: client.release_address(**params) ip_released = True - except botocore.exceptions.ClientError: - pass + except botocore.exceptions.ClientError as e: + err_msg = str(e) - return ip_released + return ip_released, err_msg def create(client, subnet_id, allocation_id, client_token=None, @@ -973,11 +977,15 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, err_msg = str(e) if release_eip: - eip_released = ( - release_address(client, allocation_id, check_mode=check_mode) + eip_released, eip_err = ( + release_address(client, allocation_id, check_mode) ) if not eip_released: - err_msg = "Failed to release EIP %s".format(allocation_id) + err_msg = ( + "{0}: Failed to release EIP {1}: {2}" + .format(err_msg, allocation_id, eip_err) + ) + success = False return success, changed, err_msg, results @@ -1037,7 +1045,6 @@ def main(): changed = False err_msg = '' - #Ensure resource is present if state == 'present': if not subnet_id: module.fail_json(msg='subnet_id is required for creation') diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py index 7c4f163ad40..1b75c88a143 100644 --- a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -392,7 +392,7 @@ class AnsibleEc2VpcNatGatewayFunctions(unittest.TestCase): def test_release_address(self): client = boto3.client('ec2', region_name=aws_region) - success = ( + success, _ = ( ng.release_address( client, 'eipalloc-1234567', check_mode=True )