diff --git a/CHANGES.rst b/CHANGES.rst index b5dde2150a..7e31592ecb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,6 +26,12 @@ Common Compute ~~~~~~~ +- [EC2] Allow Elastic IP association with a specific network interface, so + instances with multiple interfaces can select the required target. Existing + calls continue to associate by instance ID. + (GITHUB-1799) + [Ishu Patel - @ishu7w] + - [OpenStack] Select service catalog endpoints by service type without filtering by the default service name. The service name is only used as a filter when explicitly provided via ``ex_force_service_name``. diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index 8d62c78739..5e1f0a7a4f 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -3290,7 +3290,7 @@ def ex_describe_all_addresses(self, only_associated=False): # shape how the return data is sent back return self._to_addresses(response, only_associated) - def ex_associate_address_with_node(self, node, elastic_ip, domain=None): + def ex_associate_address_with_node(self, node, elastic_ip, domain=None, network_interface=None): """ Associate an Elastic IP address with a particular node. @@ -3303,12 +3303,26 @@ def ex_associate_address_with_node(self, node, elastic_ip, domain=None): :param domain: The domain where the IP resides (vpc only) :type domain: ``str`` + :param network_interface: Optional interface to associate the IP + with instead of the node. Required for nodes with multiple + network interfaces. + :type network_interface: :class:`EC2NetworkInterface` + + For a VPC address on an instance with multiple interfaces, pass + ``domain="vpc"`` and the desired interface from + :meth:`ex_list_network_interfaces`. The request uses its interface ID + instead of the instance ID, as required by EC2. + :return: A string representation of the association ID which is required for VPC disassociation. EC2/standard addresses return None :rtype: ``None`` or ``str`` """ - params = {"Action": "AssociateAddress", "InstanceId": node.id} + params = {"Action": "AssociateAddress"} + if network_interface is not None: + params["NetworkInterfaceId"] = network_interface.id + else: + params["InstanceId"] = node.id if domain is not None and domain != "vpc": raise AttributeError("Domain can only be set to vpc") @@ -3322,13 +3336,15 @@ def ex_associate_address_with_node(self, node, elastic_ip, domain=None): association_id = findtext(element=response, xpath="associationId", namespace=NAMESPACE) return association_id - def ex_associate_addresses(self, node, elastic_ip, domain=None): + def ex_associate_addresses(self, node, elastic_ip, domain=None, network_interface=None): """ Note: This method has been deprecated in favor of the ex_associate_address_with_node method. """ - return self.ex_associate_address_with_node(node=node, elastic_ip=elastic_ip, domain=domain) + return self.ex_associate_address_with_node( + node=node, elastic_ip=elastic_ip, domain=domain, network_interface=network_interface + ) def ex_disassociate_address(self, elastic_ip, domain=None): """ diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py index b01ef2e098..16440d5f3a 100644 --- a/libcloud/test/compute/test_ec2.py +++ b/libcloud/test/compute/test_ec2.py @@ -46,6 +46,7 @@ EC2NetworkSubnet, NimbusNodeDriver, EC2PlacementGroup, + EC2NetworkInterface, IdempotentParamError, ExEC2AvailabilityZone, OutscaleSASNodeDriver, @@ -886,6 +887,21 @@ def test_ex_associate_address_with_node(self): domain="bogus", ) + def test_ex_associate_address_with_network_interface(self): + node = Node("i-4382922a", None, None, None, None, self.driver) + interface = EC2NetworkInterface("eni-12345678", "secondary", "attached") + EC2MockHttp.type = "all_addresses" + elastic_ip = self.driver.ex_describe_all_addresses()[3] + EC2MockHttp.type = "interface" + association_id = self.driver.ex_associate_address_with_node( + node, elastic_ip, domain="vpc", network_interface=interface + ) + self.assertEqual("eipassoc-167a8073", association_id) + association_id = self.driver.ex_associate_addresses( + node, elastic_ip, domain="vpc", network_interface=interface + ) + self.assertEqual("eipassoc-167a8073", association_id) + def test_ex_disassociate_address(self): EC2MockHttp.type = "all_addresses" elastic_ips = self.driver.ex_describe_all_addresses() @@ -1681,6 +1697,18 @@ def _AssociateAddress(self, method, url, body, headers): return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _vpc_AssociateAddress(self, method, url, body, headers): + params = parse_qs(url.split("?", 1)[1]) + self.assertEqual(["i-4382922a"], params["InstanceId"]) + self.assertNotIn("NetworkInterfaceId", params) + body = self.fixtures.load("associate_vpc_address.xml") + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _interface_AssociateAddress(self, method, url, body, headers): + params = parse_qs(url.split("?", 1)[1]) + self.assertEqual(["eni-12345678"], params["NetworkInterfaceId"]) + self.assertNotIn("InstanceId", params) + self.assertIn("AllocationId", params) + self.assertNotIn("PublicIp", params) body = self.fixtures.load("associate_vpc_address.xml") return (httplib.OK, body, {}, httplib.responses[httplib.OK])