mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Add secondary IP support and allow specifying sec groups by name (#2161)
This commit is contained in:
parent
6f6927380e
commit
aa36ed8612
1 changed files with 119 additions and 33 deletions
|
@ -20,7 +20,7 @@ short_description: Create and optionally attach an Elastic Network Interface (EN
|
||||||
description:
|
description:
|
||||||
- Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance.
|
- Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance.
|
||||||
version_added: "2.0"
|
version_added: "2.0"
|
||||||
author: Rob White, wimnat [at] gmail.com, @wimnat
|
author: "Rob White (@wimnat)"
|
||||||
options:
|
options:
|
||||||
eni_id:
|
eni_id:
|
||||||
description:
|
description:
|
||||||
|
@ -48,7 +48,8 @@ options:
|
||||||
default: null
|
default: null
|
||||||
security_groups:
|
security_groups:
|
||||||
description:
|
description:
|
||||||
- List of security groups associated with the interface. Only used when state=present.
|
- List of security groups associated with the interface. Only used when state=present. Since version 2.2, you \
|
||||||
|
can specify security groups by ID or by name or a combination of both. Prior to 2.2, you can specify only by ID.
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
state:
|
state:
|
||||||
|
@ -75,7 +76,19 @@ options:
|
||||||
description:
|
description:
|
||||||
- By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation.
|
- By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation.
|
||||||
required: false
|
required: false
|
||||||
extends_documentation_fragment: aws
|
secondary_private_ip_addresses:
|
||||||
|
description:
|
||||||
|
- A list of IP addresses to assign as secondary IP addresses to the network interface. This option is mutually exclusive of secondary_private_ip_address_count
|
||||||
|
required: false
|
||||||
|
version_added: 2.2
|
||||||
|
secondary_private_ip_address_count:
|
||||||
|
description:
|
||||||
|
- The number of secondary IP addresses to assign to the network interface. This option is mutually exclusive of secondary_private_ip_addresses
|
||||||
|
required: false
|
||||||
|
version_added: 2.2
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- aws
|
||||||
|
- ec2
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
|
@ -95,6 +108,29 @@ EXAMPLES = '''
|
||||||
subnet_id: subnet-xxxxxxxx
|
subnet_id: subnet-xxxxxxxx
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
# Create an ENI with two secondary addresses
|
||||||
|
- ec2_eni:
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
state: present
|
||||||
|
secondary_private_ip_address_count: 2
|
||||||
|
|
||||||
|
# Assign a secondary IP address to an existing ENI
|
||||||
|
# This will purge any existing IPs
|
||||||
|
- ec2_eni:
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
eni_id: eni-yyyyyyyy
|
||||||
|
state: present
|
||||||
|
secondary_private_ip_addresses:
|
||||||
|
- 172.16.1.1
|
||||||
|
|
||||||
|
# Remove any secondary IP addresses from an existing ENI
|
||||||
|
- ec2_eni:
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
eni_id: eni-yyyyyyyy
|
||||||
|
state: present
|
||||||
|
secondary_private_ip_addresses:
|
||||||
|
-
|
||||||
|
|
||||||
# Destroy an ENI, detaching it from any instance if necessary
|
# Destroy an ENI, detaching it from any instance if necessary
|
||||||
- ec2_eni:
|
- ec2_eni:
|
||||||
eni_id: eni-xxxxxxx
|
eni_id: eni-xxxxxxx
|
||||||
|
@ -131,26 +167,24 @@ EXAMPLES = '''
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import boto.ec2
|
import boto.ec2
|
||||||
|
import boto.vpc
|
||||||
from boto.exception import BotoServerError
|
from boto.exception import BotoServerError
|
||||||
HAS_BOTO = True
|
HAS_BOTO = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_BOTO = False
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
|
||||||
def get_error_message(xml_string):
|
|
||||||
|
|
||||||
root = ET.fromstring(xml_string)
|
|
||||||
for message in root.findall('.//Message'):
|
|
||||||
return message.text
|
|
||||||
|
|
||||||
|
|
||||||
def get_eni_info(interface):
|
def get_eni_info(interface):
|
||||||
|
|
||||||
|
# Private addresses
|
||||||
|
private_addresses = []
|
||||||
|
for ip in interface.private_ip_addresses:
|
||||||
|
private_addresses.append({ 'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary })
|
||||||
|
|
||||||
interface_info = {'id': interface.id,
|
interface_info = {'id': interface.id,
|
||||||
'subnet_id': interface.subnet_id,
|
'subnet_id': interface.subnet_id,
|
||||||
'vpc_id': interface.vpc_id,
|
'vpc_id': interface.vpc_id,
|
||||||
|
@ -161,6 +195,7 @@ def get_eni_info(interface):
|
||||||
'private_ip_address': interface.private_ip_address,
|
'private_ip_address': interface.private_ip_address,
|
||||||
'source_dest_check': interface.source_dest_check,
|
'source_dest_check': interface.source_dest_check,
|
||||||
'groups': dict((group.id, group.name) for group in interface.groups),
|
'groups': dict((group.id, group.name) for group in interface.groups),
|
||||||
|
'private_ip_addresses': private_addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
if interface.attachment is not None:
|
if interface.attachment is not None:
|
||||||
|
@ -174,6 +209,7 @@ def get_eni_info(interface):
|
||||||
|
|
||||||
return interface_info
|
return interface_info
|
||||||
|
|
||||||
|
|
||||||
def wait_for_eni(eni, status):
|
def wait_for_eni(eni, status):
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
@ -188,7 +224,7 @@ def wait_for_eni(eni, status):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def create_eni(connection, module):
|
def create_eni(connection, vpc_id, module):
|
||||||
|
|
||||||
instance_id = module.params.get("instance_id")
|
instance_id = module.params.get("instance_id")
|
||||||
if instance_id == 'None':
|
if instance_id == 'None':
|
||||||
|
@ -197,7 +233,9 @@ def create_eni(connection, module):
|
||||||
subnet_id = module.params.get('subnet_id')
|
subnet_id = module.params.get('subnet_id')
|
||||||
private_ip_address = module.params.get('private_ip_address')
|
private_ip_address = module.params.get('private_ip_address')
|
||||||
description = module.params.get('description')
|
description = module.params.get('description')
|
||||||
security_groups = module.params.get('security_groups')
|
security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False)
|
||||||
|
secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses")
|
||||||
|
secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count")
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -213,15 +251,30 @@ def create_eni(connection, module):
|
||||||
# Wait to allow creation / attachment to finish
|
# Wait to allow creation / attachment to finish
|
||||||
wait_for_eni(eni, "attached")
|
wait_for_eni(eni, "attached")
|
||||||
eni.update()
|
eni.update()
|
||||||
|
|
||||||
|
if secondary_private_ip_address_count is not None:
|
||||||
|
try:
|
||||||
|
connection.assign_private_ip_addresses(network_interface_id=eni.id, secondary_private_ip_address_count=secondary_private_ip_address_count)
|
||||||
|
except BotoServerError:
|
||||||
|
eni.delete()
|
||||||
|
raise
|
||||||
|
|
||||||
|
if secondary_private_ip_addresses is not None:
|
||||||
|
try:
|
||||||
|
connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses)
|
||||||
|
except BotoServerError:
|
||||||
|
eni.delete()
|
||||||
|
raise
|
||||||
|
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
except BotoServerError as e:
|
except BotoServerError as e:
|
||||||
module.fail_json(msg=get_error_message(e.args[2]))
|
module.fail_json(msg=e.message)
|
||||||
|
|
||||||
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
||||||
|
|
||||||
|
|
||||||
def modify_eni(connection, module):
|
def modify_eni(connection, vpc_id, module):
|
||||||
|
|
||||||
eni_id = module.params.get("eni_id")
|
eni_id = module.params.get("eni_id")
|
||||||
instance_id = module.params.get("instance_id")
|
instance_id = module.params.get("instance_id")
|
||||||
|
@ -232,10 +285,12 @@ def modify_eni(connection, module):
|
||||||
do_detach = False
|
do_detach = False
|
||||||
device_index = module.params.get("device_index")
|
device_index = module.params.get("device_index")
|
||||||
description = module.params.get('description')
|
description = module.params.get('description')
|
||||||
security_groups = module.params.get('security_groups')
|
security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False)
|
||||||
force_detach = module.params.get("force_detach")
|
force_detach = module.params.get("force_detach")
|
||||||
source_dest_check = module.params.get("source_dest_check")
|
source_dest_check = module.params.get("source_dest_check")
|
||||||
delete_on_termination = module.params.get("delete_on_termination")
|
delete_on_termination = module.params.get("delete_on_termination")
|
||||||
|
secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses")
|
||||||
|
secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count")
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -261,6 +316,24 @@ def modify_eni(connection, module):
|
||||||
changed = True
|
changed = True
|
||||||
else:
|
else:
|
||||||
module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached")
|
module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached")
|
||||||
|
|
||||||
|
current_secondary_addresses = [i.private_ip_address for i in eni.private_ip_addresses if not i.primary]
|
||||||
|
if secondary_private_ip_addresses is not None:
|
||||||
|
secondary_addresses_to_remove = list(set(current_secondary_addresses) - set(secondary_private_ip_addresses))
|
||||||
|
if secondary_addresses_to_remove:
|
||||||
|
connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)), dry_run=False)
|
||||||
|
connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses, secondary_private_ip_address_count=None, allow_reassignment=False, dry_run=False)
|
||||||
|
if secondary_private_ip_address_count is not None:
|
||||||
|
current_secondary_address_count = len(current_secondary_addresses)
|
||||||
|
|
||||||
|
if secondary_private_ip_address_count > current_secondary_address_count:
|
||||||
|
connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=None, secondary_private_ip_address_count=(secondary_private_ip_address_count - current_secondary_address_count), allow_reassignment=False, dry_run=False)
|
||||||
|
changed = True
|
||||||
|
elif secondary_private_ip_address_count < current_secondary_address_count:
|
||||||
|
# How many of these addresses do we want to remove
|
||||||
|
secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count
|
||||||
|
connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False)
|
||||||
|
|
||||||
if eni.attachment is not None and instance_id is None and do_detach is True:
|
if eni.attachment is not None and instance_id is None and do_detach is True:
|
||||||
eni.detach(force_detach)
|
eni.detach(force_detach)
|
||||||
wait_for_eni(eni, "detached")
|
wait_for_eni(eni, "detached")
|
||||||
|
@ -272,8 +345,7 @@ def modify_eni(connection, module):
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
except BotoServerError as e:
|
except BotoServerError as e:
|
||||||
print e
|
module.fail_json(msg=e.message)
|
||||||
module.fail_json(msg=get_error_message(e.args[2]))
|
|
||||||
|
|
||||||
eni.update()
|
eni.update()
|
||||||
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
||||||
|
@ -302,12 +374,12 @@ def delete_eni(connection, module):
|
||||||
|
|
||||||
module.exit_json(changed=changed)
|
module.exit_json(changed=changed)
|
||||||
except BotoServerError as e:
|
except BotoServerError as e:
|
||||||
msg = get_error_message(e.args[2])
|
|
||||||
regex = re.compile('The networkInterface ID \'.*\' does not exist')
|
regex = re.compile('The networkInterface ID \'.*\' does not exist')
|
||||||
if regex.search(msg) is not None:
|
if regex.search(e.message) is not None:
|
||||||
module.exit_json(changed=False)
|
module.exit_json(changed=False)
|
||||||
else:
|
else:
|
||||||
module.fail_json(msg=get_error_message(e.args[2]))
|
module.fail_json(msg=e.message)
|
||||||
|
|
||||||
|
|
||||||
def compare_eni(connection, module):
|
def compare_eni(connection, module):
|
||||||
|
|
||||||
|
@ -326,10 +398,11 @@ def compare_eni(connection, module):
|
||||||
return eni
|
return eni
|
||||||
|
|
||||||
except BotoServerError as e:
|
except BotoServerError as e:
|
||||||
module.fail_json(msg=get_error_message(e.args[2]))
|
module.fail_json(msg=e.message)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_sec_group_list(groups):
|
def get_sec_group_list(groups):
|
||||||
|
|
||||||
# Build list of remote security groups
|
# Build list of remote security groups
|
||||||
|
@ -340,6 +413,14 @@ def get_sec_group_list(groups):
|
||||||
return remote_security_groups
|
return remote_security_groups
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vpc_id(conn, subnet_id):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return conn.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id
|
||||||
|
except BotoServerError as e:
|
||||||
|
module.fail_json(msg=e.message)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argument_spec = ec2_argument_spec()
|
argument_spec = ec2_argument_spec()
|
||||||
argument_spec.update(
|
argument_spec.update(
|
||||||
|
@ -354,11 +435,18 @@ def main():
|
||||||
state = dict(default='present', choices=['present', 'absent']),
|
state = dict(default='present', choices=['present', 'absent']),
|
||||||
force_detach = dict(default='no', type='bool'),
|
force_detach = dict(default='no', type='bool'),
|
||||||
source_dest_check = dict(default=None, type='bool'),
|
source_dest_check = dict(default=None, type='bool'),
|
||||||
delete_on_termination = dict(default=None, type='bool')
|
delete_on_termination = dict(default=None, type='bool'),
|
||||||
|
secondary_private_ip_addresses = dict(default=None, type='list'),
|
||||||
|
secondary_private_ip_address_count = dict(default=None, type='int')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=argument_spec)
|
module = AnsibleModule(argument_spec=argument_spec,
|
||||||
|
required_if = ([
|
||||||
|
('state', 'present', ['subnet_id']),
|
||||||
|
('state', 'absent', ['eni_id']),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
if not HAS_BOTO:
|
if not HAS_BOTO:
|
||||||
module.fail_json(msg='boto required for this module')
|
module.fail_json(msg='boto required for this module')
|
||||||
|
@ -368,6 +456,7 @@ def main():
|
||||||
if region:
|
if region:
|
||||||
try:
|
try:
|
||||||
connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||||
|
vpc_connection = connect_to_aws(boto.vpc, region, **aws_connect_params)
|
||||||
except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e:
|
except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e:
|
||||||
module.fail_json(msg=str(e))
|
module.fail_json(msg=str(e))
|
||||||
else:
|
else:
|
||||||
|
@ -377,16 +466,13 @@ def main():
|
||||||
eni_id = module.params.get("eni_id")
|
eni_id = module.params.get("eni_id")
|
||||||
|
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
|
subnet_id = module.params.get("subnet_id")
|
||||||
|
vpc_id = _get_vpc_id(vpc_connection, subnet_id)
|
||||||
if eni_id is None:
|
if eni_id is None:
|
||||||
if module.params.get("subnet_id") is None:
|
create_eni(connection, vpc_id, module)
|
||||||
module.fail_json(msg="subnet_id must be specified when state=present")
|
|
||||||
create_eni(connection, module)
|
|
||||||
else:
|
else:
|
||||||
modify_eni(connection, module)
|
modify_eni(connection, vpc_id, module)
|
||||||
elif state == 'absent':
|
elif state == 'absent':
|
||||||
if eni_id is None:
|
|
||||||
module.fail_json(msg="eni_id must be specified")
|
|
||||||
else:
|
|
||||||
delete_eni(connection, module)
|
delete_eni(connection, module)
|
||||||
|
|
||||||
from ansible.module_utils.basic import *
|
from ansible.module_utils.basic import *
|
||||||
|
|
Loading…
Reference in a new issue