Repository: ansible/ansible

Issue:
ec2_eip does not allow reassociation

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
ec2_eip

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.1.0
```

##### OS / ENVIRONMENT
N/A 
(Ubuntu 16.04)

##### SUMMARY
<!--- Explain the problem briefly -->

ec2_eip does not allow to reallocate an already associated Elastic IP.

##### STEPS TO REPRODUCE

- Have an associated ElasticIP
- Try associate it with another device

<!--- Paste example playbooks or commands between quotes below -->
```yaml
    - name: Reassociate IP address to
      ec2_eip:
        public_ip: 1.2.3.4
        state: present
        device_id: "foo"
        private_ip_address: 'bar'
        in_vpc: yes
```

##### EXPECTED RESULTS

The ElasticIP is associated to the new device

##### ACTUAL RESULTS

<!--- Paste verbatim command output between quotes below -->
```
    "msg": "EC2ResponseError: 400 Bad Request

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Errors>
    <Error>
      <Code>Resource.AlreadyAssociated</Code>
      <Message>resource eipalloc-xxxxx is already associated with associate-id eipassoc-yyy</Message>
    </Error>
  </Errors>
</Response>
```

The [`boto` method](http://boto.cloudhackers.com/en/latest/ref/ec2.html#boto.ec2.connection.EC2Connection.associate_address) used under the hood does allow the reallocation.


Discussion:
I’ll submit a pull-request for this
cc @rickmendes
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/ec2_eip.py b/lib/ansible/modules/cloud/amazon/ec2_eip.py
--- a/lib/ansible/modules/cloud/amazon/ec2_eip.py
+++ b/lib/ansible/modules/cloud/amazon/ec2_eip.py
@@ -72,6 +72,13 @@
     required: False
     default: None
     version_added: "2.3"
+  allow_reassociation:
+    description:
+      -  Specify this option to allow an Elastic IP address that is already associated with another
+         network interface or instance to be re-associated with the specified instance or interface.
+    required: false
+    default: false
+    version_added: "2.5"
 extends_documentation_fragment:
     - aws
     - ec2
@@ -101,6 +108,12 @@
     device_id: eni-c8ad70f3
     ip: 93.184.216.119
 
+- name: associate an elastic IP with a device and allow reassociation
+  ec2_eip:
+    device_id: eni-c8ad70f3
+    public_ip: 93.184.216.119
+    allow_reassociation: yes
+
 - name: disassociate an elastic IP from an instance
   ec2_eip:
     device_id: i-1212f003
@@ -167,7 +180,7 @@ class EIPException(Exception):
     pass
 
 
-def associate_ip_and_device(ec2, address, private_ip_address, device_id, check_mode, isinstance=True):
+def associate_ip_and_device(ec2, address, private_ip_address, device_id, allow_reassociation, check_mode, isinstance=True):
     if address_is_associated_with_device(ec2, address, device_id, isinstance):
         return {'changed': False}
 
@@ -175,11 +188,20 @@ def associate_ip_and_device(ec2, address, private_ip_address, device_id, check_m
     if not check_mode:
         if isinstance:
             if address.domain == "vpc":
-                res = ec2.associate_address(device_id, allocation_id=address.allocation_id, private_ip_address=private_ip_address)
+                res = ec2.associate_address(device_id,
+                                            allocation_id=address.allocation_id,
+                                            private_ip_address=private_ip_address,
+                                            allow_reassociation=allow_reassociation)
             else:
-                res = ec2.associate_address(device_id, public_ip=address.public_ip, private_ip_address=private_ip_address)
+                res = ec2.associate_address(device_id,
+                                            public_ip=address.public_ip,
+                                            private_ip_address=private_ip_address,
+                                            allow_reassociation=allow_reassociation)
         else:
-            res = ec2.associate_address(network_interface_id=device_id, allocation_id=address.allocation_id, private_ip_address=private_ip_address)
+            res = ec2.associate_address(network_interface_id=device_id,
+                                        allocation_id=address.allocation_id,
+                                        private_ip_address=private_ip_address,
+                                        allow_reassociation=allow_reassociation)
         if not res:
             raise EIPException('association failed')
 
@@ -297,7 +319,7 @@ def find_device(ec2, module, device_id, isinstance=True):
 
 
 def ensure_present(ec2, module, domain, address, private_ip_address, device_id,
-                   reuse_existing_ip_allowed, check_mode, isinstance=True):
+                   reuse_existing_ip_allowed, allow_reassociation, check_mode, isinstance=True):
     changed = False
 
     # Return the EIP object since we've been given a public IP
@@ -315,12 +337,12 @@ def ensure_present(ec2, module, domain, address, private_ip_address, device_id,
                 if instance.vpc_id and len(instance.vpc_id) > 0 and domain is None:
                     raise EIPException("You must set 'in_vpc' to true to associate an instance with an existing ip in a vpc")
             # Associate address object (provided or allocated) with instance
-            assoc_result = associate_ip_and_device(ec2, address, private_ip_address, device_id,
+            assoc_result = associate_ip_and_device(ec2, address, private_ip_address, device_id, allow_reassociation,
                                                  check_mode)
         else:
             instance = find_device(ec2, module, device_id, isinstance=False)
             # Associate address object (provided or allocated) with instance
-            assoc_result = associate_ip_and_device(ec2, address, private_ip_address, device_id,
+            assoc_result = associate_ip_and_device(ec2, address, private_ip_address, device_id, allow_reassociation,
                                                  check_mode, isinstance=False)
 
         if instance.vpc_id:
@@ -359,6 +381,7 @@ def main():
         reuse_existing_ip_allowed=dict(required=False, type='bool',
                                        default=False),
         release_on_disassociation=dict(required=False, type='bool', default=False),
+        allow_reassociation=dict(type='bool', default=False),
         wait_timeout=dict(default=300),
         private_ip_address=dict(required=False, default=None, type='str')
     ))
@@ -382,6 +405,7 @@ def main():
     domain = 'vpc' if in_vpc else None
     reuse_existing_ip_allowed = module.params.get('reuse_existing_ip_allowed')
     release_on_disassociation = module.params.get('release_on_disassociation')
+    allow_reassociation = module.params.get('allow_reassociation')
 
     # Parameter checks
     if private_ip_address is not None and device_id is None:
@@ -408,7 +432,7 @@ def main():
         if state == 'present':
             if device_id:
                 result = ensure_present(ec2, module, domain, address, private_ip_address, device_id,
-                                    reuse_existing_ip_allowed, module.check_mode, isinstance=is_instance)
+                                    reuse_existing_ip_allowed, allow_reassociation, module.check_mode, isinstance=is_instance)
             else:
                 if address:
                     changed = False


Test patch: