From 27c4beb19db83d812b56a0635dc5a0373bc975bb Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Thu, 2 Jun 2016 16:36:19 -0300 Subject: [PATCH 1/7] Fix the AMI creation/modification logic thus making it idempotent --- cloud/amazon/ec2_eni.py | 126 ++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 69 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 8403cbbbe7b..f9d0e528c6d 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,7 +18,9 @@ DOCUMENTATION = ''' module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance 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 state=detached, an ENI can be detached from its instance. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -29,7 +31,8 @@ options: default: null instance_id: description: - - Instance ID that you wish to attach ENI to. To detach an ENI from an instance, use 'None'. + - Instance ID that you wish to attach ENI to, if None the new ENI will be + created in detached state, existing ENI will keep current attachment state. required: false default: null private_ip_address: @@ -54,10 +57,10 @@ options: default: null state: description: - - Create or delete ENI. + - Create, delete or detach ENI from its instance. required: false default: present - choices: [ 'present', 'absent' ] + choices: [ 'present', 'absent', 'detached' ] device_index: description: - The index of the device for the network interface attachment on the instance. @@ -129,7 +132,7 @@ EXAMPLES = ''' eni_id: eni-yyyyyyyy state: present secondary_private_ip_addresses: - - + - # Destroy an ENI, detaching it from any instance if necessary - ec2_eni: @@ -239,7 +242,7 @@ def create_eni(connection, vpc_id, module): changed = False try: - eni = compare_eni(connection, module) + eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) if instance_id is not None: @@ -274,21 +277,13 @@ def create_eni(connection, vpc_id, module): module.exit_json(changed=changed, interface=get_eni_info(eni)) -def modify_eni(connection, vpc_id, module, looked_up_eni_id): +def modify_eni(connection, vpc_id, module, eni): - if looked_up_eni_id is None: - eni_id = module.params.get("eni_id") - else: - eni_id = looked_up_eni_id instance_id = module.params.get("instance_id") - if instance_id == 'None': - instance_id = None - do_detach = True - else: - do_detach = False + do_detach = module.params.get('state') == 'detached' device_index = module.params.get("device_index") description = module.params.get('description') - security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) + security_groups = module.params.get('security_groups') 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") @@ -297,28 +292,23 @@ def modify_eni(connection, vpc_id, module, looked_up_eni_id): changed = False try: - # Get the eni with the eni_id specified - eni_result_set = connection.get_all_network_interfaces(eni_id) - eni = eni_result_set[0] if description is not None: if eni.description != description: connection.modify_network_interface_attribute(eni.id, "description", description) changed = True - if security_groups is not None: - if sorted(get_sec_group_list(eni.groups)) != sorted(security_groups): - connection.modify_network_interface_attribute(eni.id, "groupSet", security_groups) + if len(security_groups) > 0: + groups = get_ec2_security_group_ids_from_names(security_groups, connection, vpc_id=vpc_id, boto3=False) + if sorted(get_sec_group_list(eni.groups)) != sorted(groups): + connection.modify_network_interface_attribute(eni.id, "groupSet", groups) changed = True if source_dest_check is not None: if eni.source_dest_check != source_dest_check: connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check) changed = True - if delete_on_termination is not None: - if eni.attachment is not None: - if eni.attachment.delete_on_termination is not delete_on_termination: - connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) - changed = True - else: - module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached") + if delete_on_termination is not None and eni.attachment is not None: + if eni.attachment.delete_on_termination is not delete_on_termination: + connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) + changed = True 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: @@ -337,15 +327,10 @@ def modify_eni(connection, vpc_id, module, looked_up_eni_id): 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") + if instance_id is not None: + eni.attach(instance_id, device_index) + wait_for_eni(eni, "attached") changed = True - else: - if instance_id is not None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True except BotoServerError as e: module.fail_json(msg=e.message) @@ -384,21 +369,36 @@ def delete_eni(connection, module): module.fail_json(msg=e.message) -def compare_eni(connection, module): +def detach_eni(connection, module): + + eni = find_eni(connection, module) + if eni.attachment is not None: + eni.detach(force_detach) + wait_for_eni(eni, "detached") + eni.update() + module.exit_json(changed=True, interface=get_eni_info(eni)) + else: + module.exit_json(changed=False, interface=get_eni_info(eni)) + + +def find_eni(connection, module): eni_id = module.params.get("eni_id") 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') try: - all_eni = connection.get_all_network_interfaces(eni_id) - - for eni in all_eni: - remote_security_groups = get_sec_group_list(eni.groups) - if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (sorted(remote_security_groups) == sorted(security_groups)): - return eni + filters = {} + if private_ip_address: + filters['private-ip-address'] = private_ip_address + if subnet_id: + filters['subnet-id'] = subnet_id + + eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) + if len(eni_result) > 0: + return eni_result[0] + else: + return None except BotoServerError as e: module.fail_json(msg=e.message) @@ -424,22 +424,6 @@ def _get_vpc_id(connection, module, subnet_id): module.fail_json(msg=e.message) -def get_eni_id_by_ip(connection, module): - - subnet_id = module.params.get('subnet_id') - private_ip_address = module.params.get('private_ip_address') - - try: - all_eni = connection.get_all_network_interfaces(filters={'private-ip-address': private_ip_address, 'subnet-id': subnet_id}) - except BotoServerError as e: - module.fail_json(msg=e.message) - - if all_eni: - return all_eni[0].id - else: - return None - - def main(): argument_spec = ec2_argument_spec() argument_spec.update( @@ -451,7 +435,7 @@ def main(): description=dict(type='str'), security_groups=dict(default=[], type='list'), device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent']), + state=dict(default='present', choices=['present', 'absent', 'detached']), force_detach=dict(default='no', type='bool'), source_dest_check=dict(default=None, type='bool'), delete_on_termination=dict(default=None, type='bool'), @@ -467,6 +451,7 @@ def main(): required_if=([ ('state', 'present', ['subnet_id']), ('state', 'absent', ['eni_id']), + ('state', 'detached', ['eni_id']), ]) ) @@ -491,16 +476,19 @@ def main(): if state == 'present': subnet_id = module.params.get("subnet_id") vpc_id = _get_vpc_id(vpc_connection, module, subnet_id) - # If private_ip_address is not None, look up to see if an ENI already exists with that IP - if eni_id is None and private_ip_address is not None: - eni_id = get_eni_id_by_ip(connection, module) - if eni_id is None: + + eni = find_eni(connection, module) + if eni is None: create_eni(connection, vpc_id, module) else: - modify_eni(connection, vpc_id, module, eni_id) + modify_eni(connection, vpc_id, module, eni) + elif state == 'absent': delete_eni(connection, module) + elif state == 'detached': + detach_eni(connection, module) + from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From 178caff2ed4e47bc04f22516d4fd2e70a11584c2 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Thu, 2 Jun 2016 17:16:02 -0300 Subject: [PATCH 2/7] Fix docs --- cloud/amazon/ec2_eni.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index f9d0e528c6d..b4d90aa7d55 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,9 +18,8 @@ DOCUMENTATION = ''' module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance 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 state=detached, an ENI can be detached from its 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 state=detached, an ENI can be detached from its instance. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -31,8 +30,8 @@ options: default: null instance_id: description: - - Instance ID that you wish to attach ENI to, if None the new ENI will be - created in detached state, existing ENI will keep current attachment state. + - Instance ID that you wish to attach ENI to, if None the new ENI will be created in detached state, existing \ + ENI will keep current attachment state. required: false default: null private_ip_address: From 84615249048f7b0d5c4a955afbb776315da43470 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Fri, 3 Jun 2016 16:10:22 -0300 Subject: [PATCH 3/7] Add RETURN docs --- cloud/amazon/ec2_eni.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index b4d90aa7d55..8fced6034f9 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -168,6 +168,60 @@ EXAMPLES = ''' ''' + +RETURN = ''' +interface: + description: Network interface attributes + returned: when state != absent + type: dictionary + contains: + description: + description: interface description + type: string + sample: Firewall network interface + groups: + description: list of security groups + type: list of dictionaries + sample: [ { "sg-f8a8a9da": "default" } ] + id: + description: network interface id + type: string + sample: "eni-1d889198" + mac_address: + description: interface's physical address + type: string + sample: "06:9a:27:6a:6f:99" + owner_id: + description: aws account id + type: string + sample: 812381371 + private_ip_address: + description: primary ip address of this interface + type: string + sample: 10.20.30.40 + private_ip_addresses: + description: list of all private ip addresses associated to this interface + type: list of dictionaries + sample: [ { "primary_address": true, "private_ip_address": "10.20.30.40" } ] + source_dest_check: + description: value of source/dest check flag + type: boolean + sample: True + status: + description: network interface status + type: string + sample: "pending" + subnet_id: + description: which vpc subnet the interface is bound + type: string + sample: subnet-b0a0393c + vpc_id: + description: which vpc this network interface is bound + type: string + sample: vpc-9a9a9da + +''' + import time import re From 3099b607c1c88adff9a17031db1b9c031c7009ae Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 14 Jun 2016 11:37:45 -0300 Subject: [PATCH 4/7] Add attached parameter to ec2_eni module --- cloud/amazon/ec2_eni.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 8fced6034f9..55a43f4da37 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -56,15 +56,21 @@ options: default: null state: description: - - Create, delete or detach ENI from its instance. + - Create or delete ENI required: false default: present - choices: [ 'present', 'absent', 'detached' ] + choices: [ 'present', 'absent' ] device_index: description: - The index of the device for the network interface attachment on the instance. required: false default: 0 + attached: + description: + - Specifies if network interface should be attached or detached from instance. If attached=yes and no \ + instance_id is given, attachment status won't change + required: false + default: yes force_detach: description: - Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent. @@ -283,6 +289,7 @@ def wait_for_eni(eni, status): def create_eni(connection, vpc_id, module): instance_id = module.params.get("instance_id") + attached = module.params.get("attached") if instance_id == 'None': instance_id = None device_index = module.params.get("device_index") @@ -298,7 +305,7 @@ def create_eni(connection, vpc_id, module): eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if instance_id is not None: + if attached and instance_id is not None: try: eni.attach(instance_id, device_index) except BotoServerError: @@ -333,6 +340,7 @@ def create_eni(connection, vpc_id, module): def modify_eni(connection, vpc_id, module, eni): instance_id = module.params.get("instance_id") + attached = module.params.get("attached") do_detach = module.params.get('state') == 'detached' device_index = module.params.get("device_index") description = module.params.get('description') @@ -380,10 +388,13 @@ def modify_eni(connection, vpc_id, module, eni): 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 instance_id is not None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True + if attached: + if instance_id is not None: + eni.attach(instance_id, device_index) + wait_for_eni(eni, "attached") + changed = True + else: + detach_eni(eni, module) except BotoServerError as e: module.fail_json(msg=e.message) @@ -422,9 +433,9 @@ def delete_eni(connection, module): module.fail_json(msg=e.message) -def detach_eni(connection, module): +def detach_eni(eni, module): - eni = find_eni(connection, module) + force_detach = module.params.get("force_detach") if eni.attachment is not None: eni.detach(force_detach) wait_for_eni(eni, "detached") @@ -488,12 +499,13 @@ def main(): description=dict(type='str'), security_groups=dict(default=[], type='list'), device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent', 'detached']), + 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'), secondary_private_ip_addresses=dict(default=None, type='list'), - secondary_private_ip_address_count=dict(default=None, type='int') + secondary_private_ip_address_count=dict(default=None, type='int'), + attached=dict(default=True, type='bool') ) ) @@ -503,8 +515,7 @@ def main(): ], required_if=([ ('state', 'present', ['subnet_id']), - ('state', 'absent', ['eni_id']), - ('state', 'detached', ['eni_id']), + ('state', 'absent', ['eni_id']) ]) ) @@ -539,9 +550,6 @@ def main(): elif state == 'absent': delete_eni(connection, module) - elif state == 'detached': - detach_eni(connection, module) - from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From c81e88856abdc253a5b4653c72e57d3948239e14 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 14 Jun 2016 14:55:54 -0300 Subject: [PATCH 5/7] Add "version_added" to attached attribute --- cloud/amazon/ec2_eni.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 55a43f4da37..378c2c6d2fd 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -71,6 +71,7 @@ options: instance_id is given, attachment status won't change required: false default: yes + version_added: 2.2 force_detach: description: - Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent. From 753ddf87ac83ba4d9786ecb5a8d46bbee30c85e4 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Wed, 15 Jun 2016 10:49:29 -0300 Subject: [PATCH 6/7] Change attached parameter default to None --- cloud/amazon/ec2_eni.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 378c2c6d2fd..b87e7c47304 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,8 +18,9 @@ DOCUMENTATION = ''' module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance 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 state=detached, an ENI can be detached from its instance. + - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID or private_ip is \ + provided, the existing ENI (if any) will be modified. The 'attached' parameter controls the attachment status \ + of the network interface. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -30,8 +31,8 @@ options: default: null instance_id: description: - - Instance ID that you wish to attach ENI to, if None the new ENI will be created in detached state, existing \ - ENI will keep current attachment state. + - Instance ID that you wish to attach ENI to. Since version 2.2, use the 'attached' parameter to attach or \ + detach an ENI. Prior to 2.2, to detach an ENI from an instance, use 'None'. required: false default: null private_ip_address: @@ -67,8 +68,8 @@ options: default: 0 attached: description: - - Specifies if network interface should be attached or detached from instance. If attached=yes and no \ - instance_id is given, attachment status won't change + - Specifies if network interface should be attached or detached from instance. If ommited, attachment status \ + won't change required: false default: yes version_added: 2.2 @@ -306,7 +307,7 @@ def create_eni(connection, vpc_id, module): eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if attached and instance_id is not None: + if attached == True and instance_id is not None: try: eni.attach(instance_id, device_index) except BotoServerError: @@ -389,12 +390,11 @@ def modify_eni(connection, vpc_id, module, eni): 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 attached: - if instance_id is not None: + if attached == True and instance_id is not None: eni.attach(instance_id, device_index) wait_for_eni(eni, "attached") changed = True - else: + elif attached == False: detach_eni(eni, module) except BotoServerError as e: @@ -506,7 +506,7 @@ def main(): 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'), - attached=dict(default=True, type='bool') + attached=dict(default=None, type='bool') ) ) @@ -516,7 +516,8 @@ def main(): ], required_if=([ ('state', 'present', ['subnet_id']), - ('state', 'absent', ['eni_id']) + ('state', 'absent', ['eni_id']), + ('attached', True, ['instance_id']) ]) ) From e2e697c3ffc1066c1d11d63c5e44384949a47fef Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 26 Jul 2016 11:38:21 -0300 Subject: [PATCH 7/7] Fix attachment issue ( thanks @gunzy83 ) --- cloud/amazon/ec2_eni.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index b87e7c47304..ac05ba43a39 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -241,7 +241,6 @@ try: except ImportError: HAS_BOTO = False - def get_eni_info(interface): # Private addresses @@ -390,7 +389,10 @@ def modify_eni(connection, vpc_id, module, eni): 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 attached == True and instance_id is not None: + if attached == True: + if eni.attachment and eni.attachment.instance_id != instance_id: + detach_eni(eni, module) + if eni.attachment is None: eni.attach(instance_id, device_index) wait_for_eni(eni, "attached") changed = True @@ -451,13 +453,20 @@ def find_eni(connection, module): eni_id = module.params.get("eni_id") subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') + instance_id = module.params.get('instance_id') + device_index = module.params.get('device_index') try: filters = {} - if private_ip_address: - filters['private-ip-address'] = private_ip_address if subnet_id: filters['subnet-id'] = subnet_id + if private_ip_address: + filters['private-ip-address'] = private_ip_address + else: + if instance_id: + filters['attachment.instance-id'] = instance_id + if device_index: + filters['attachment.device-index'] = device_index eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) if len(eni_result) > 0: