Add secondary IP support and allow specifying sec groups by name (#2161)

This commit is contained in:
Rob 2016-05-06 08:26:40 +10:00 committed by Matt Clay
parent 6f6927380e
commit aa36ed8612

View file

@ -20,7 +20,7 @@ short_description: Create and optionally attach an Elastic Network Interface (EN
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.
version_added: "2.0"
author: Rob White, wimnat [at] gmail.com, @wimnat
author: "Rob White (@wimnat)"
options:
eni_id:
description:
@ -48,7 +48,8 @@ options:
default: null
security_groups:
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
default: null
state:
@ -74,8 +75,20 @@ options:
source_dest_check:
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.
required: false
extends_documentation_fragment: aws
required: false
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 = '''
@ -95,6 +108,29 @@ EXAMPLES = '''
subnet_id: subnet-xxxxxxxx
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
- ec2_eni:
eni_id: eni-xxxxxxx
@ -131,26 +167,24 @@ EXAMPLES = '''
'''
import time
import xml.etree.ElementTree as ET
import re
try:
import boto.ec2
import boto.vpc
from boto.exception import BotoServerError
HAS_BOTO = True
except ImportError:
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):
# 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,
'subnet_id': interface.subnet_id,
'vpc_id': interface.vpc_id,
@ -161,6 +195,7 @@ def get_eni_info(interface):
'private_ip_address': interface.private_ip_address,
'source_dest_check': interface.source_dest_check,
'groups': dict((group.id, group.name) for group in interface.groups),
'private_ip_addresses': private_addresses
}
if interface.attachment is not None:
@ -174,6 +209,7 @@ def get_eni_info(interface):
return interface_info
def wait_for_eni(eni, status):
while True:
@ -188,7 +224,7 @@ def wait_for_eni(eni, status):
break
def create_eni(connection, module):
def create_eni(connection, vpc_id, module):
instance_id = module.params.get("instance_id")
if instance_id == 'None':
@ -197,7 +233,9 @@ def create_eni(connection, module):
subnet_id = module.params.get('subnet_id')
private_ip_address = module.params.get('private_ip_address')
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
try:
@ -213,15 +251,30 @@ def create_eni(connection, module):
# Wait to allow creation / attachment to finish
wait_for_eni(eni, "attached")
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
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))
def modify_eni(connection, module):
def modify_eni(connection, vpc_id, module):
eni_id = module.params.get("eni_id")
instance_id = module.params.get("instance_id")
@ -232,10 +285,12 @@ def modify_eni(connection, module):
do_detach = False
device_index = module.params.get("device_index")
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")
source_dest_check = module.params.get("source_dest_check")
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
try:
@ -261,6 +316,24 @@ def modify_eni(connection, module):
changed = True
else:
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:
eni.detach(force_detach)
wait_for_eni(eni, "detached")
@ -272,8 +345,7 @@ def modify_eni(connection, module):
changed = True
except BotoServerError as e:
print e
module.fail_json(msg=get_error_message(e.args[2]))
module.fail_json(msg=e.message)
eni.update()
module.exit_json(changed=changed, interface=get_eni_info(eni))
@ -302,12 +374,12 @@ def delete_eni(connection, module):
module.exit_json(changed=changed)
except BotoServerError as e:
msg = get_error_message(e.args[2])
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)
else:
module.fail_json(msg=get_error_message(e.args[2]))
module.fail_json(msg=e.message)
def compare_eni(connection, module):
@ -326,10 +398,11 @@ def compare_eni(connection, module):
return eni
except BotoServerError as e:
module.fail_json(msg=get_error_message(e.args[2]))
module.fail_json(msg=e.message)
return None
def get_sec_group_list(groups):
# Build list of remote security groups
@ -340,6 +413,14 @@ def get_sec_group_list(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():
argument_spec = ec2_argument_spec()
argument_spec.update(
@ -354,11 +435,18 @@ def main():
state = dict(default='present', choices=['present', 'absent']),
force_detach = dict(default='no', 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:
module.fail_json(msg='boto required for this module')
@ -368,6 +456,7 @@ def main():
if region:
try:
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:
module.fail_json(msg=str(e))
else:
@ -377,17 +466,14 @@ def main():
eni_id = module.params.get("eni_id")
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 module.params.get("subnet_id") is None:
module.fail_json(msg="subnet_id must be specified when state=present")
create_eni(connection, module)
create_eni(connection, vpc_id, module)
else:
modify_eni(connection, module)
modify_eni(connection, vpc_id, module)
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.ec2 import *