diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 30d05f85554..58f1b0caf72 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -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: @@ -75,6 +76,16 @@ options: 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 + 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 @@ -97,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 @@ -133,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, @@ -163,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: @@ -176,6 +209,7 @@ def get_eni_info(interface): return interface_info + def wait_for_eni(eni, status): while True: @@ -190,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': @@ -199,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: @@ -215,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") @@ -234,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: @@ -263,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") @@ -274,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)) @@ -304,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): @@ -328,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 @@ -342,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( @@ -356,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') @@ -370,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: @@ -379,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 *