Skip to content

Commit

Permalink
Add ability to remove detached internet gateway (#1786)
Browse files Browse the repository at this point in the history
Add ability to remove detached internet gateway

SUMMARY

Introduces the ability to remove an internet gateway that is not attached to a VPC. To remove an internet gateway either the ID of the internet gateway or the ID of the attached VPC can be supplied. It is also possible to supply both IDs. If both IDs are supplied then a failure will be generated if the attached VPC ID does not match the user supplied VPC.

Fixes #1669
ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ec2_vpc_igw
ADDITIONAL INFORMATION



I wasn't able to add a test for removing an internet gateway that is detached as the module currently doesn't have a way to create a detached internet gateway. I plan on opening a follow-up PR for that use case and will add an appropriate removal test in that PR.

Reviewed-by: Mark Chappell
Reviewed-by: Brant Evans <[email protected]>
Reviewed-by: Brian A. Teller
  • Loading branch information
branic authored Oct 31, 2023
1 parent c9fd4b7 commit 35ea344
Show file tree
Hide file tree
Showing 4 changed files with 492 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
minor_changes:
- ec2_vpc_igw - Add ability to delete a vpc internet gateway using the id of the gateway (https://github.com/ansible-collections/amazon.aws/pull/1786).
- ec2_vpc_igw - Add ability to create an internet gateway without attaching a VPC (https://github.com/ansible-collections/amazon.aws/pull/1786).
- ec2_vpc_igw - Add ability to attach/detach VPC to/from internet gateway (https://github.com/ansible-collections/amazon.aws/pull/1786).
- ec2_vpc_igw - Add ability to change VPC attached to internet gateway (https://github.com/ansible-collections/amazon.aws/pull/1786).
206 changes: 185 additions & 21 deletions plugins/modules/ec2_vpc_igw.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,39 @@
- Manage an AWS VPC Internet gateway
author: Robert Estelle (@erydo)
options:
internet_gateway_id:
version_added: 7.0.0
description:
- The ID of Internet Gateway to manage.
required: false
type: str
vpc_id:
description:
- The VPC ID for the VPC in which to manage the Internet Gateway.
required: true
- The VPC ID for the VPC to attach (when state=present)
- VPC ID can also be provided to find the internet gateway to manage that the VPC is attached to
required: false
type: str
state:
description:
- Create or terminate the IGW
default: present
choices: [ 'present', 'absent' ]
type: str
force_attach:
version_added: 7.0.0
description:
- Force attaching VPC to I(vpc_id).
- Setting this option to true will detach an existing VPC attachment and attach to the supplied I(vpc_id).
- Ignored when I(state=absent).
- I(vpc_id) must be specified when I(force_attach) is true
default: false
type: bool
detach_vpc:
version_added: 7.0.0
description:
- Remove attached VPC from gateway
default: false
type: bool
notes:
- Support for I(purge_tags) was added in release 1.3.0.
extends_documentation_fragment:
Expand Down Expand Up @@ -53,11 +75,38 @@
Tag2: tag2
register: igw
- name: Delete Internet gateway
- name: Create a detached gateway
amazon.aws.ec2_vpc_igw:
state: present
register: igw
- name: Change the VPC the gateway is attached to
amazon.aws.ec2_vpc_igw:
internet_gateway_id: igw-abcdefgh
vpc_id: vpc-stuvwxyz
force_attach: true
state: present
register: igw
- name: Delete Internet gateway using the attached vpc id
amazon.aws.ec2_vpc_igw:
state: absent
vpc_id: vpc-abcdefgh
register: vpc_igw_delete
- name: Delete Internet gateway with gateway id
amazon.aws.ec2_vpc_igw:
state: absent
internet_gateway_id: igw-abcdefgh
register: vpc_igw_delete
- name: Delete Internet gateway ensuring attached VPC is correct
amazon.aws.ec2_vpc_igw:
state: absent
internet_gateway_id: igw-abcdefgh
vpc_id: vpc-abcdefgh
register: vpc_igw_delete
"""

RETURN = r"""
Expand Down Expand Up @@ -109,6 +158,11 @@ def describe_igws_with_backoff(connection, **params):
return paginator.paginate(**params).build_full_result()["InternetGateways"]


def describe_vpcs_with_backoff(connection, **params):
paginator = connection.get_paginator("describe_vpcs")
return paginator.paginate(**params).build_full_result()["Vpcs"]


class AnsibleEc2Igw:
def __init__(self, module, results):
self._module = module
Expand All @@ -117,15 +171,18 @@ def __init__(self, module, results):
self._check_mode = self._module.check_mode

def process(self):
internet_gateway_id = self._module.params.get("internet_gateway_id")
vpc_id = self._module.params.get("vpc_id")
state = self._module.params.get("state", "present")
tags = self._module.params.get("tags")
purge_tags = self._module.params.get("purge_tags")
force_attach = self._module.params.get("force_attach")
detach_vpc = self._module.params.get("detach_vpc")

if state == "present":
self.ensure_igw_present(vpc_id, tags, purge_tags)
self.ensure_igw_present(internet_gateway_id, vpc_id, tags, purge_tags, force_attach, detach_vpc)
elif state == "absent":
self.ensure_igw_absent(vpc_id)
self.ensure_igw_absent(internet_gateway_id, vpc_id)

def get_matching_igw(self, vpc_id, gateway_id=None):
"""
Expand All @@ -136,11 +193,11 @@ def get_matching_igw(self, vpc_id, gateway_id=None):
Returns:
igw (dict): dict of igw found, None if none found
"""
filters = ansible_dict_to_boto3_filter_list({"attachment.vpc-id": vpc_id})
try:
# If we know the gateway_id, use it to avoid bugs with using filters
# See https://github.com/ansible-collections/amazon.aws/pull/766
if not gateway_id:
filters = ansible_dict_to_boto3_filter_list({"attachment.vpc-id": vpc_id})
igws = describe_igws_with_backoff(self._connection, Filters=filters)
else:
igws = describe_igws_with_backoff(self._connection, InternetGatewayIds=[gateway_id])
Expand All @@ -155,6 +212,30 @@ def get_matching_igw(self, vpc_id, gateway_id=None):

return igw

def get_matching_vpc(self, vpc_id):
"""
Returns the virtual private cloud found.
Parameters:
vpc_id (str): VPC ID
Returns:
vpc (dict): dict of vpc found, None if none found
"""
try:
vpcs = describe_vpcs_with_backoff(self._connection, VpcIds=[vpc_id])
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
# self._module.fail_json(msg=f"{str(e)}")
if "InvalidVpcID.NotFound" in str(e):
self._module.fail_json(msg=f"VPC with Id {vpc_id} not found, aborting")
self._module.fail_json_aws(e)

vpc = None
if len(vpcs) > 1:
self._module.fail_json(msg=f"EC2 returned more than one VPC for {vpc_id}, aborting")
elif vpcs:
vpc = camel_dict_to_snake_dict(vpcs[0])

return vpc

@staticmethod
def get_igw_info(igw, vpc_id):
return {
Expand All @@ -163,55 +244,124 @@ def get_igw_info(igw, vpc_id):
"vpc_id": vpc_id,
}

def ensure_igw_absent(self, vpc_id):
igw = self.get_matching_igw(vpc_id)
def detach_vpc(self, igw_id, vpc_id):
try:
self._connection.detach_internet_gateway(aws_retry=True, InternetGatewayId=igw_id, VpcId=vpc_id)

self._results["changed"] = True
except botocore.exceptions.WaiterError as e:
self._module.fail_json_aws(e, msg="Unable to detach VPC.")

def attach_vpc(self, igw_id, vpc_id):
try:
self._connection.attach_internet_gateway(aws_retry=True, InternetGatewayId=igw_id, VpcId=vpc_id)

# Ensure the gateway is attached before proceeding
waiter = get_waiter(self._connection, "internet_gateway_attached")
waiter.wait(InternetGatewayIds=[igw_id])

self._results["changed"] = True
except botocore.exceptions.WaiterError as e:
self._module.fail_json_aws(e, msg="Failed to attach VPC.")

def ensure_igw_absent(self, igw_id, vpc_id):
igw = self.get_matching_igw(vpc_id, gateway_id=igw_id)
if igw is None:
return self._results

igw_vpc_id = ""

if len(igw["attachments"]) > 0:
igw_vpc_id = igw["attachments"][0]["vpc_id"]

if vpc_id and (igw_vpc_id != vpc_id):
self._module.fail_json(msg=f"Supplied VPC ({vpc_id}) does not match found VPC ({igw_vpc_id}), aborting")

if self._check_mode:
self._results["changed"] = True
return self._results

try:
self._results["changed"] = True
self._connection.detach_internet_gateway(
aws_retry=True, InternetGatewayId=igw["internet_gateway_id"], VpcId=vpc_id
)

if igw_vpc_id:
self.detach_vpc(igw["internet_gateway_id"], igw_vpc_id)

self._connection.delete_internet_gateway(aws_retry=True, InternetGatewayId=igw["internet_gateway_id"])
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
self._module.fail_json_aws(e, msg="Unable to delete Internet Gateway")

return self._results

def ensure_igw_present(self, vpc_id, tags, purge_tags):
igw = self.get_matching_igw(vpc_id)
def ensure_igw_present(self, igw_id, vpc_id, tags, purge_tags, force_attach, detach_vpc):
igw = None

if igw_id:
igw = self.get_matching_igw(None, gateway_id=igw_id)
elif vpc_id:
igw = self.get_matching_igw(vpc_id)

if igw is None:
if self._check_mode:
self._results["changed"] = True
self._results["gateway_id"] = None
return self._results

if vpc_id:
self.get_matching_vpc(vpc_id)

try:
response = self._connection.create_internet_gateway(aws_retry=True)

# Ensure the gateway exists before trying to attach it or add tags
waiter = get_waiter(self._connection, "internet_gateway_exists")
waiter.wait(InternetGatewayIds=[response["InternetGateway"]["InternetGatewayId"]])
self._results["changed"] = True

igw = camel_dict_to_snake_dict(response["InternetGateway"])
self._connection.attach_internet_gateway(
aws_retry=True, InternetGatewayId=igw["internet_gateway_id"], VpcId=vpc_id
)

# Ensure the gateway is attached before proceeding
waiter = get_waiter(self._connection, "internet_gateway_attached")
waiter.wait(InternetGatewayIds=[igw["internet_gateway_id"]])
self._results["changed"] = True
if vpc_id:
self.attach_vpc(igw["internet_gateway_id"], vpc_id)
except botocore.exceptions.WaiterError as e:
self._module.fail_json_aws(e, msg="No Internet Gateway exists.")
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
self._module.fail_json_aws(e, msg="Unable to create Internet Gateway")
else:
igw_vpc_id = None

if len(igw["attachments"]) > 0:
igw_vpc_id = igw["attachments"][0]["vpc_id"]

if detach_vpc:
if self._check_mode:
self._results["changed"] = True
self._results["gateway_id"] = igw["internet_gateway_id"]
return self._results

self.detach_vpc(igw["internet_gateway_id"], igw_vpc_id)

elif igw_vpc_id != vpc_id:
if self._check_mode:
self._results["changed"] = True
self._results["gateway_id"] = igw["internet_gateway_id"]
return self._results

if force_attach:
self.get_matching_vpc(vpc_id)

self.detach_vpc(igw["internet_gateway_id"], igw_vpc_id)
self.attach_vpc(igw["internet_gateway_id"], vpc_id)
else:
self._module.fail_json(msg="VPC already attached, but does not match requested VPC.")

elif vpc_id:
if self._check_mode:
self._results["changed"] = True
self._results["gateway_id"] = igw["internet_gateway_id"]
return self._results

self.get_matching_vpc(vpc_id)
self.attach_vpc(igw["internet_gateway_id"], vpc_id)

# Modify tags
self._results["changed"] |= ensure_ec2_tags(
Expand All @@ -234,16 +384,30 @@ def ensure_igw_present(self, vpc_id, tags, purge_tags):

def main():
argument_spec = dict(
vpc_id=dict(required=True),
internet_gateway_id=dict(),
vpc_id=dict(),
state=dict(default="present", choices=["present", "absent"]),
tags=dict(required=False, type="dict", aliases=["resource_tags"]),
purge_tags=dict(default=True, type="bool"),
force_attach=dict(default=False, type="bool"),
detach_vpc=dict(default=False, type="bool"),
)

required_if = [
("force_attach", True, ("vpc_id",), False),
("state", "absent", ("internet_gateway_id", "vpc_id"), True),
("detach_vpc", True, ("internet_gateway_id", "vpc_id"), True),
]

mutually_exclusive = [("force_attach", "detach_vpc")]

module = AnsibleAWSModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
)

results = dict(changed=False)
igw_manager = AnsibleEc2Igw(module=module, results=results)
igw_manager.process()
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/targets/ec2_vpc_igw/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
vpc_name: '{{ resource_prefix }}-vpc'
vpc_seed: '{{ resource_prefix }}'
vpc_cidr: 10.{{ 256 | random(seed=vpc_seed) }}.0.0/16
vpc_name_2: '{{ tiny_prefix }}-vpc-2'
vpc_seed_2: '{{ tiny_prefix }}'
vpc_cidr_2: 10.{{ 256 | random(seed=vpc_seed_2) }}.0.0/16
Loading

0 comments on commit 35ea344

Please sign in to comment.