Created
May 5, 2016 15:49
-
-
Save jonhadfield/c74672f604eaec5a1383d0af7cd0bb19 to your computer and use it in GitHub Desktop.
ec2_vpc_nat_gateway with backported camel_dict_to_snake_dict
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# This file is part of Ansible | |
# | |
# Ansible is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# Ansible is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. | |
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" | |
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 | |
required: false | |
default: None | |
subnet_id: | |
description: | |
- The id of the subnet to create the NAT Gateway in | |
required: false | |
default: None | |
allocation_id: | |
description: | |
- The id of the elastic IP allocation | |
required: false | |
default: None | |
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 | |
author: | |
- "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. | |
# Ensure that a VPC NAT Gateway exists in a subnet by passing the subnet id and EIP allocation id | |
ec2_vpc_nat_gateway: | |
state: present | |
subnet_id: subnet-a67643d1 | |
allocation_id: eipalloc-a32baec6 | |
# Ensure that a VPC NAT Gateway exists in a subnet by passing the subnet id and | |
# EIP allocation id but do not wait for operation to complete before continuing | |
ec2_vpc_nat_gateway: | |
state: present | |
subnet_id: subnet-a67643d1 | |
allocation_id: eipalloc-a32baec6 | |
wait: false | |
# Ensure that a VPC NAT Gateway identified by id does not exist | |
ec2_vpc_nat_gateway: | |
state: absent | |
nat_gateway_id: nat-0d1e3a878585988f8 | |
''' | |
RETURN = ''' | |
nat_gateway: | |
description: details of the nat gateway created | |
returned: when created | |
type: dict | |
sample: { | |
"nat_gateway": { | |
"create_time": "2016-05-02T21:21:50.683000+00:00", | |
"nat_gateway_addresses": [ | |
{ | |
"allocation_id": "eipalloc-784f391d" | |
} | |
], | |
"nat_gateway_id": "nat-03afc583f2b2962b6", | |
"state": "pending", | |
"subnet_id": "subnet-16640561", | |
"vpc_id": "vpc-6a31bc0f" | |
} | |
} | |
nat_gateway_id: | |
description: id of the VPC NAT Gateway | |
returned: On create when NAT GW already exists and on completed deletion | |
type: string | |
sample: "nat-0d1e3a878585988f8" | |
''' | |
import datetime | |
import time | |
try: | |
import boto3 | |
import botocore | |
HAS_BOTO_3 = True | |
except ImportError: | |
HAS_BOTO_3 = False | |
from distutils.version import LooseVersion | |
if LooseVersion(botocore.__version__) < LooseVersion("1.3.14"): | |
HAS_SUFFICIENT_BOTOCORE = False | |
else: | |
HAS_SUFFICIENT_BOTOCORE = True | |
def camel_dict_to_snake_dict(camel_dict): | |
def camel_to_snake(name): | |
import re | |
first_cap_re = re.compile('(.)([A-Z][a-z]+)') | |
all_cap_re = re.compile('([a-z0-9])([A-Z])') | |
s1 = first_cap_re.sub(r'\1_\2', name) | |
return all_cap_re.sub(r'\1_\2', s1).lower() | |
def value_is_list(camel_list): | |
checked_list = [] | |
for item in camel_list: | |
if isinstance(item, dict): | |
checked_list.append(camel_dict_to_snake_dict(item)) | |
elif isinstance(item, list): | |
checked_list.append(value_is_list(item)) | |
else: | |
checked_list.append(item) | |
return checked_list | |
snake_dict = {} | |
for k, v in camel_dict.iteritems(): | |
if isinstance(v, dict): | |
snake_dict[camel_to_snake(k)] = camel_dict_to_snake_dict(v) | |
elif isinstance(v, list): | |
snake_dict[camel_to_snake(k)] = value_is_list(v) | |
else: | |
snake_dict[camel_to_snake(k)] = v | |
return snake_dict | |
def get_nat_gateway_status_list(ec2_client=None, module=None): | |
""" Check if one or more NAT gateways exist with the specified attributes and return a list of matching """ | |
try: | |
subnet_id = module.params.get('subnet_id') | |
allocation_id = module.params.get('allocation_id') | |
# We only pass nat_gateway_id alone if we are ensuring it is absent | |
if module.params.get('nat_gateway_id'): | |
result = ec2_client.describe_nat_gateways(NatGatewayIds=[module.params.get('nat_gateway_id')]) | |
return [{'state': result.get('NatGateways')[0].get('State'), | |
'created': result.get('NatGateways')[0].get('CreateTime')}] | |
# Checking if VPC NAT Gateway matching attributes is present | |
elif all((subnet_id, allocation_id)): | |
result = ec2_client.describe_nat_gateways(Filter=[ | |
{ | |
'Name': 'subnet-id', | |
'Values': [subnet_id] | |
} | |
]) | |
if result.get('NatGateways'): | |
gateway_list = list() | |
for nat_gateway in result.get('NatGateways'): | |
for nat_gateway_address in nat_gateway.get('NatGatewayAddresses'): | |
if nat_gateway_address.get('AllocationId') == allocation_id: | |
gateway_list.append({'state': nat_gateway.get('State'), | |
'created': nat_gateway.get('CreateTime'), | |
'nat_gateway_id': nat_gateway.get('NatGatewayId')}) | |
return gateway_list | |
return [{'state': 'absent'}] | |
except botocore.exceptions.ClientError as ce: | |
if ce.response['Error']['Code'] == "NatGatewayNotFound": | |
return [{'state': 'absent'}] | |
else: | |
module.fail_json(msg=ce.message) | |
def delete_nat_gateway(ec2_client=None, module=None, nat_gateway=None): | |
""" Delete an existing NAT gateway """ | |
nat_gateway_address = nat_gateway.get('NatGatewayAddresses')[0] | |
nat_gateway_id = nat_gateway['NatGatewayId'] | |
results = dict(changed=True, nat_gateway_id=nat_gateway_id, | |
public_ip=nat_gateway_address.get('PublicIp'), | |
private_ip=nat_gateway_address.get('PrivateIp'), | |
allocation_id=nat_gateway_address.get('AllocationId')) | |
ec2_client.delete_nat_gateway(NatGatewayId=nat_gateway_id) | |
wait = module.params.get('wait') | |
if wait: | |
wait_timeout = time.time() + module.params.get('wait_timeout') | |
while wait_timeout > time.time(): | |
nat_gateway_status_list = get_nat_gateway_status_list(ec2_client=ec2_client, module=module) | |
if nat_gateway_status_list[0].get('state') in ('deleted', 'absent'): | |
module.exit_json(**results) | |
else: | |
time.sleep(5) | |
module.fail_json(msg="Waited too long for VPC NAT Gateway to be deleted.") | |
else: | |
module.exit_json(**results) | |
def create_nat_gateway(ec2_client=None, module=None): | |
""" Create the NAT gateway """ | |
try: | |
result = ec2_client.create_nat_gateway(SubnetId=module.params.get('subnet_id'), | |
AllocationId=module.params.get('allocation_id')) | |
wait = module.params.get('wait') | |
start_time = datetime.datetime.utcnow() | |
results = dict(changed=True) | |
if wait: | |
wait_timeout = time.time() + module.params.get('wait_timeout') | |
while wait_timeout > time.time(): | |
if 'nat_gateway' in results: | |
break | |
for present_status in get_nat_gateway_status_list(ec2_client=ec2_client, module=module): | |
create_time = present_status.get('created') | |
if present_status.get('state') == 'failed' and create_time.replace(tzinfo=None) >= start_time: | |
module.fail_json(msg="Failed to create VPC NAT Gateway") | |
elif present_status.get('state') == 'available': | |
results['nat_gateway'] = camel_dict_to_snake_dict(result['NatGateway']) | |
break | |
else: | |
time.sleep(5) | |
else: | |
module.fail_json(msg="Waited too long for VPC NAT Gateway to be created.") | |
module.exit_json(**results) | |
except botocore.exceptions.ClientError as ce: | |
module.fail_json(msg=ce.message) | |
def ensure_nat_gateway_absent(ec2_client=None, module=None): | |
""" Ensure the specified NAT gateway does not exist and call delete if it does """ | |
try: | |
results = dict(changed=False) | |
nat_gateways_result = ec2_client.describe_nat_gateways(NatGatewayIds=[module.params.get('nat_gateway_id')]) | |
if nat_gateways_result: | |
nat_gateway = nat_gateways_result.get('NatGateways')[0] | |
if nat_gateway.get('State') in ('pending', | |
'available'): | |
if module.check_mode: | |
results['changed'] = True | |
module.exit_json(**results) | |
else: | |
delete_nat_gateway(ec2_client=ec2_client, | |
module=module, | |
nat_gateway=nat_gateway) | |
module.exit_json(**results) | |
except botocore.exceptions.ClientError as ce: | |
if ce.response['Error']['Code'] == "NatGatewayMalformed": | |
module.fail_json(msg="Invalid NAT Gateway ID") | |
else: | |
module.fail_json(msg=ce.message) | |
def ensure_nat_gateway_present(ec2_client=None, module=None): | |
""" Ensure NAT gateway with specified parameters exists and call create if it does not """ | |
# Check allocation-id refers to existing EIP allocation | |
allocation_id = module.params.get('allocation_id') | |
try: | |
ec2_client.describe_addresses(AllocationIds=[module.params.get('allocation_id')]) | |
except botocore.exceptions.ClientError: | |
module.fail_json(msg="allocation: %s does not exist." % allocation_id) | |
# Check subnet exists | |
subnet_id = module.params.get('subnet_id') | |
try: | |
ec2_client.describe_subnets(SubnetIds=[subnet_id]) | |
except botocore.exceptions.ClientError: | |
module.fail_json(msg="subnet: %s does not exist." % subnet_id) | |
try: | |
gateway_status_list = get_nat_gateway_status_list(ec2_client=ec2_client, module=module) | |
for gateway_status in gateway_status_list: | |
if gateway_status.get('state') == 'available': | |
results = dict(changed=False, nat_gateway_id=gateway_status['nat_gateway_id']) | |
module.exit_json(**results) | |
else: | |
if not module.check_mode: | |
create_nat_gateway(ec2_client=ec2_client, module=module) | |
else: | |
module.exit_json(dict(change=True)) | |
except botocore.exceptions.ClientError as ce: | |
if ce.response['Error']['Code'] == "NatGatewayLimitExceeded": | |
module.fail_json(msg="The NAT Gateway limit has been exceeded.") | |
except botocore.exceptions.NoCredentialsError: | |
module.fail_json(msg="Unable to locate AWS credentials.") | |
except Exception as ce: | |
module.fail_json(msg=ce.message) | |
def main(): | |
argument_spec = ec2_argument_spec() | |
argument_spec.update(dict( | |
state=dict(default='present', choices=['present', 'absent']), | |
subnet_id=dict(required=False, type='str'), | |
allocation_id=dict(required=False, type='str'), | |
nat_gateway_id=dict(required=False, type='str'), | |
wait=dict(required=False, default=True, type='bool'), | |
wait_timeout=dict(required=False, default=300, type='int'))) | |
module = AnsibleModule(argument_spec=argument_spec, | |
supports_check_mode=True, | |
required_if=[ | |
('state', 'present', ['subnet_id', 'allocation_id']), | |
('state', 'absent', ['nat_gateway_id']) | |
]) | |
if not HAS_BOTO_3: | |
module.fail_json(msg='boto3 and botocore are required.') | |
if not HAS_SUFFICIENT_BOTOCORE: | |
module.fail_json(msg='botocore version 1.3.14 or above is required.') | |
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) | |
try: | |
ec2_client = boto3_conn(module, | |
conn_type='client', | |
resource='ec2', | |
region=region, | |
endpoint=ec2_url, | |
**aws_connect_kwargs) | |
except botocore.exceptions.NoRegionError: | |
module.fail_json(msg="AWS Region not specified") | |
if module.params['state'] == 'absent': | |
ensure_nat_gateway_absent(ec2_client=ec2_client, module=module) | |
else: | |
ensure_nat_gateway_present(ec2_client=ec2_client, module=module) | |
from ansible.module_utils.basic import * | |
from ansible.module_utils.ec2 import * | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment