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

pull/18777/head
Rob 9 years ago committed by Matt Clay
parent 6f6927380e
commit aa36ed8612

@ -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…
Cancel
Save