Updated Branches: refs/heads/trunk 621ccdd9a -> b7f709f14
Issue LIBCLOUD-474: Add Network Interface Support to the EC2 driver. Signed-off-by: Tomaz Muraus <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/8c948876 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/8c948876 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/8c948876 Branch: refs/heads/trunk Commit: 8c94887679102266c1f810b4f4e9a6d3df84542f Parents: 621ccdd Author: Chris DeRamus <[email protected]> Authored: Fri Dec 27 17:02:18 2013 -0500 Committer: Tomaz Muraus <[email protected]> Committed: Fri Jan 3 03:17:52 2014 +0100 ---------------------------------------------------------------------- libcloud/compute/drivers/ec2.py | 297 +++++++++++++++++++ .../fixtures/ec2/attach_network_interface.xml | 4 + .../fixtures/ec2/create_network_interface.xml | 30 ++ .../fixtures/ec2/delete_network_interface.xml | 4 + .../ec2/describe_network_interfaces.xml | 83 ++++++ .../fixtures/ec2/detach_network_interface.xml | 4 + libcloud/test/compute/test_ec2.py | 62 ++++ 7 files changed, 484 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/compute/drivers/ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index 3d52214..c116ea5 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -840,6 +840,26 @@ class EC2NetworkSubnet(object): return (('<EC2NetworkSubnet: id=%s, name=%s') % (self.id, self.name)) +class EC2NetworkInterface(object): + """ + Represents information about a VPC network interface + + Note: This class is EC2 specific. The state parameter denotes the current + status of the interface. Valid values for state are attaching, attached, + detaching and detached. + """ + + def __init__(self, id, name, state, extra=None): + self.id = id + self.name = name + self.state = state + self.extra = extra or {} + + def __repr__(self): + return (('<EC2NetworkInterface: id=%s, name=%s') + % (self.id, self.name)) + + class BaseEC2NodeDriver(NodeDriver): """ Base Amazon EC2 node driver. @@ -1182,6 +1202,148 @@ class BaseEC2NodeDriver(NodeDriver): return EC2NetworkSubnet(subnet_id, name, state, extra=extra) + def _to_interfaces(self, response): + return [self._to_interface(el) for el in response.findall( + fixxpath(xpath='networkInterfaceSet/item', namespace=NAMESPACE)) + ] + + def _to_interface(self, element, name=None): + """ + Parse the XML element and return a EC2NetworkInterface object. + + :param name: An optional name for the interface. If not provided + then either tag with a key "Name" or the interface ID + will be used (whichever is available first in that + order). + :type name: ``str`` + + :rtype: :class: `EC2NetworkInterface` + """ + + interface_id = findtext(element=element, + xpath='networkInterfaceId', + namespace=NAMESPACE) + + state = findtext(element=element, + xpath='status', + namespace=NAMESPACE) + + # Get tags + tags = self._get_resource_tags(element) + + name = name if name else tags.get('Name', interface_id) + + # Build security groups + groups = [] + for item in findall(element=element, + xpath='groupSet/item', + namespace=NAMESPACE): + + groups.append({'group_id': findtext(element=item, + xpath='groupId', + namespace=NAMESPACE), + 'group_name': findtext(element=item, + xpath='groupName', + namespace=NAMESPACE)}) + + # Build private IPs + priv_ips = [] + for item in findall(element=element, + xpath='privateIpAddressesSet/item', + namespace=NAMESPACE): + + priv_ips.append({'private_ip': findtext(element=item, + xpath='privateIpAddress', + namespace=NAMESPACE), + 'private_dns': findtext(element=item, + xpath='privateDnsName', + namespace=NAMESPACE), + 'primary': findtext(element=item, + xpath='primary', + namespace=NAMESPACE)}) + + # Build our attachment extra attributes map + attachment_attributes_map = { + 'attachment_id': { + 'xpath': 'attachment/attachmentId', + 'transform_func': str + }, + 'instance_id': { + 'xpath': 'attachment/instanceId', + 'transform_func': str + }, + 'owner_id': { + 'xpath': 'attachment/instanceOwnerId', + 'transform_func': str + }, + 'device_index': { + 'xpath': 'attachment/deviceIndex', + 'transform_func': int + }, + 'status': { + 'xpath': 'attachment/status', + 'transform_func': str + }, + 'attach_time': { + 'xpath': 'attachment/attachTime', + 'transform_func': parse_date + }, + 'delete': { + 'xpath': 'attachment/deleteOnTermination', + 'transform_func': str + }, + } + + # Build our attachment dictionary which we will add into extra later + attachment = self._get_extra_dict(element, attachment_attributes_map) + + # Build our extra attributes map + extra_attributes_map = { + 'subnet_id': { + 'xpath': 'subnetId', + 'transform_func': str + }, + 'vpc_id': { + 'xpath': 'vpcId', + 'transform_func': str + }, + 'zone': { + 'xpath': 'availabilityZone', + 'transform_func': str + }, + 'description': { + 'xpath': 'description', + 'transform_func': str + }, + 'owner_id': { + 'xpath': 'ownerId', + 'transform_func': str + }, + 'mac_address': { + 'xpath': 'macAddress', + 'transform_func': str + }, + 'private_dns_name': { + 'xpath': 'privateIpAddressesSet/privateDnsName', + 'transform_func': str + }, + 'source_dest_check': { + 'xpath': 'sourceDestCheck', + 'transform_func': str + } + } + + # Build our extra dict + extra = self._get_extra_dict(element, extra_attributes_map) + + # Include our previously built items as well + extra['tags'] = tags + extra['attachment'] = attachment + extra['private_ips'] = priv_ips + extra['groups'] = groups + + return EC2NetworkInterface(interface_id, name, state, extra=extra) + def ex_list_reserved_nodes(self): """ List all reserved instances/nodes which can be purchased from Amazon @@ -2584,6 +2746,141 @@ class BaseEC2NodeDriver(NodeDriver): node_elastic_ips = self.ex_describe_addresses([node]) return node_elastic_ips[node.id] + def ex_list_network_interfaces(self): + """ + Return all network interfaces + + :return: List of EC2NetworkInterface instances + :rtype: ``list`` of :class `EC2NetworkInterface` + """ + params = {'Action': 'DescribeNetworkInterfaces'} + + return self._to_interfaces( + self.connection.request(self.path, params=params).object + ) + + def ex_create_network_interface(self, subnet, name=None, + description=None, + private_ip_address=None): + """ + Create a network interface within a VPC subnet. + + :param node: EC2NetworkSubnet instance + :type node: :class:`EC2NetworkSubnet` + + :param name: Optional name of the interface + :type name: ``str`` + + :param description: Optional description of the network interface + :type description: ``str`` + + :param private_ip_address: Optional address to assign as the + primary private IP address of the + interface. If one is not provided then + Amazon will automatically auto-assign + an available IP. EC2 allows assignment + of multiple IPs, but this will be + the primary. + :type private_ip_address: ``str`` + + :return: EC2NetworkInterface instance + :rtype: :class `EC2NetworkInterface` + """ + params = {'Action': 'CreateNetworkInterface', + 'SubnetId': subnet.id} + + if description: + params['Description'] = description + + if private_ip_address: + params['PrivateIpAddress'] = private_ip_address + + response = self.connection.request(self.path, params=params).object + + element = response.findall(fixxpath(xpath='networkInterface', + namespace=NAMESPACE))[0] + + interface = self._to_interface(element, name) + + if name is not None: + tags = {'Name': name} + self.ex_create_tags(resource=interface, tags=tags) + + return interface + + def ex_delete_network_interface(self, network_interface): + """ + Deletes a network interface. + + :param network_interface: EC2NetworkInterface instance + :type network_interface: :class:`EC2NetworkInterface` + + :rtype: ``bool`` + """ + params = {'Action': 'DeleteNetworkInterface', + 'NetworkInterfaceId': network_interface.id} + + result = self.connection.request(self.path, params=params).object + element = findtext(element=result, xpath='return', + namespace=NAMESPACE) + + return element == 'true' + + def ex_attach_network_interface_to_node(self, network_interface, + node, device_index): + """ + Attatch a network interface to an instance. + + :param network_interface: EC2NetworkInterface instance + :type network_interface: :class:`EC2NetworkInterface` + + :param node: Node instance + :type node: :class:`Node` + + :param device_index: The interface device index + :type device_index: ``int`` + + :return: String representation of the attachment id. + This is required to detach the interface. + :rtype: ``str`` + """ + params = {'Action': 'AttachNetworkInterface', + 'NetworkInterfaceId': network_interface.id, + 'InstanceId': node.id, + 'DeviceIndex': device_index} + + response = self.connection.request(self.path, params=params).object + attachment_id = findattr(element=response, xpath='attachmentId', + namespace=NAMESPACE) + + return attachment_id + + def ex_detach_network_interface(self, attachment_id, force=False): + """ + Detatch a network interface from an instance. + + :param attachment_id: The attachment ID associated with the + interface + :type attachment_id: ``str`` + + :param force: Forces the detachment. + :type force: ``bool`` + + :return: ``True`` on successful detachment, ``False`` otherwise. + :rtype: ``bool`` + """ + params = {'Action': 'DetachNetworkInterface', + 'AttachmentId': attachment_id} + + if force: + params['Force'] = True + + result = self.connection.request(self.path, params=params).object + + element = findtext(element=result, xpath='return', + namespace=NAMESPACE) + return element == 'true' + def ex_modify_instance_attribute(self, node, attributes): """ Modify node attributes. http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/fixtures/ec2/attach_network_interface.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/attach_network_interface.xml b/libcloud/test/compute/fixtures/ec2/attach_network_interface.xml new file mode 100644 index 0000000..7b6baa8 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/attach_network_interface.xml @@ -0,0 +1,4 @@ +<AttachNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/"> + <requestId>e46c7abc-8b14-4315-99cb-773a0f95d833</requestId> + <attachmentId>eni-attach-2b588b47</attachmentId> +</AttachNetworkInterfaceResponse> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/fixtures/ec2/create_network_interface.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/create_network_interface.xml b/libcloud/test/compute/fixtures/ec2/create_network_interface.xml new file mode 100644 index 0000000..416477e --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/create_network_interface.xml @@ -0,0 +1,30 @@ +<CreateNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/"> + <requestId>ca764ebe-8abc-4d37-9995-b9e88c086fa8</requestId> + <networkInterface> + <networkInterfaceId>eni-2b36086d</networkInterfaceId> + <subnetId>subnet-5ed9d432</subnetId> + <vpcId>vpc-62ded30e</vpcId> + <availabilityZone>us-east-1d</availabilityZone> + <description>My Test</description> + <ownerId>123456789098</ownerId> + <requesterManaged>false</requesterManaged> + <status>pending</status> + <macAddress>0e:bd:49:3e:11:74</macAddress> + <privateIpAddress>172.16.4.144</privateIpAddress> + <privateDnsName>ip-172-16-4-144.ec2.internal</privateDnsName> + <sourceDestCheck>true</sourceDestCheck> + <groupSet> + <item> + <groupId>sg-495a9926</groupId> + <groupName>default</groupName> + </item> + </groupSet> + <tagSet/> + <privateIpAddressesSet> + <item> + <privateIpAddress>172.16.4.144</privateIpAddress> + <primary>true</primary> + </item> + </privateIpAddressesSet> + </networkInterface> +</CreateNetworkInterfaceResponse> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/fixtures/ec2/delete_network_interface.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/delete_network_interface.xml b/libcloud/test/compute/fixtures/ec2/delete_network_interface.xml new file mode 100644 index 0000000..07bc619 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/delete_network_interface.xml @@ -0,0 +1,4 @@ +<DeleteNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/"> + <requestId>c0bc0036-e328-47c6-bd6d-318b007f66ee</requestId> + <return>true</return> +</DeleteNetworkInterfaceResponse> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/fixtures/ec2/describe_network_interfaces.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/describe_network_interfaces.xml b/libcloud/test/compute/fixtures/ec2/describe_network_interfaces.xml new file mode 100644 index 0000000..f520877 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/describe_network_interfaces.xml @@ -0,0 +1,83 @@ +<DescribeNetworkInterfacesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/"> + <requestId>e8fc6c0b-d6f8-4b85-aa29-e6a097eb4631</requestId> + <networkInterfaceSet> + <item> + <networkInterfaceId>eni-18e6c05e</networkInterfaceId> + <subnetId>subnet-5ed9d432</subnetId> + <vpcId>vpc-62ded30e</vpcId> + <availabilityZone>us-east-1d</availabilityZone> + <description>Test Interface 1</description> + <ownerId>123456789098</ownerId> + <requesterManaged>false</requesterManaged> + <status>in-use</status> + <macAddress>0e:6e:df:72:78:af</macAddress> + <privateIpAddress>172.16.4.133</privateIpAddress> + <privateDnsName>ip-172-16-4-133.ec2.internal</privateDnsName> + <sourceDestCheck>true</sourceDestCheck> + <groupSet> + <item> + <groupId>sg-495a9926</groupId> + <groupName>default</groupName> + </item> + </groupSet> + <attachment> + <attachmentId>eni-attach-c87dd1a4</attachmentId> + <instanceId>i-caa71db1</instanceId> + <instanceOwnerId>123456789098</instanceOwnerId> + <deviceIndex>1</deviceIndex> + <status>attached</status> + <attachTime>2013-12-02T17:46:27.000Z</attachTime> + <deleteOnTermination>false</deleteOnTermination> + </attachment> + <tagSet/> + <privateIpAddressesSet> + <item> + <privateIpAddress>172.16.4.133</privateIpAddress> + <privateDnsName>ip-172-16-4-133.ec2.internal</privateDnsName> + <primary>true</primary> + </item> + </privateIpAddressesSet> + </item> + <item> + <networkInterfaceId>eni-83e3c5c5</networkInterfaceId> + <subnetId>subnet-5ed9d432</subnetId> + <vpcId>vpc-62ded30e</vpcId> + <availabilityZone>us-east-1d</availabilityZone> + <description/> + <ownerId>123456789098</ownerId> + <requesterManaged>false</requesterManaged> + <status>in-use</status> + <macAddress>0e:93:0b:e9:e9:c4</macAddress> + <privateIpAddress>172.16.4.145</privateIpAddress> + <privateDnsName>ip-172-16-4-145.ec2.internal</privateDnsName> + <sourceDestCheck>true</sourceDestCheck> + <groupSet> + <item> + <groupId>sg-13e4607c</groupId> + <groupName>Test Group</groupName> + </item> + <item> + <groupId>sg-495a9926</groupId> + <groupName>default</groupName> + </item> + </groupSet> + <attachment> + <attachmentId>eni-attach-bae984d6</attachmentId> + <instanceId>i-caa71db1</instanceId> + <instanceOwnerId>123456789098</instanceOwnerId> + <deviceIndex>0</deviceIndex> + <status>attached</status> + <attachTime>2013-11-25T13:35:03.000Z</attachTime> + <deleteOnTermination>true</deleteOnTermination> + </attachment> + <tagSet/> + <privateIpAddressesSet> + <item> + <privateIpAddress>172.16.4.145</privateIpAddress> + <privateDnsName>ip-172-16-4-145.ec2.internal</privateDnsName> + <primary>true</primary> + </item> + </privateIpAddressesSet> + </item> + </networkInterfaceSet> +</DescribeNetworkInterfacesResponse> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/fixtures/ec2/detach_network_interface.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/detach_network_interface.xml b/libcloud/test/compute/fixtures/ec2/detach_network_interface.xml new file mode 100644 index 0000000..0f95a8b --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/detach_network_interface.xml @@ -0,0 +1,4 @@ +<DetachNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/"> + <requestId>1a683cd6-58ea-4b93-a6e9-a23b56afddf0</requestId> + <return>true</return> +</DetachNetworkInterfaceResponse> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c948876/libcloud/test/compute/test_ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py index b68f944..e8e4a14 100644 --- a/libcloud/test/compute/test_ec2.py +++ b/libcloud/test/compute/test_ec2.py @@ -868,6 +868,48 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): resp = self.driver.ex_get_console_output(node) self.assertEqual('Test String', resp['output']) + def test_ex_list_network_interfaces(self): + interfaces = self.driver.ex_list_network_interfaces() + + self.assertEqual(len(interfaces), 2) + + self.assertEqual('eni-18e6c05e', interfaces[0].id) + self.assertEqual('in-use', interfaces[0].state) + self.assertEqual('0e:6e:df:72:78:af', + interfaces[0].extra['mac_address']) + + self.assertEqual('eni-83e3c5c5', interfaces[1].id) + self.assertEqual('in-use', interfaces[1].state) + self.assertEqual('0e:93:0b:e9:e9:c4', + interfaces[1].extra['mac_address']) + + def test_ex_create_network_interface(self): + subnet = self.driver.ex_list_subnets()[0] + interface = self.driver.ex_create_network_interface( + subnet, + name='Test Interface', + description='My Test') + + self.assertEqual('eni-2b36086d', interface.id) + self.assertEqual('pending', interface.state) + self.assertEqual('0e:bd:49:3e:11:74', interface.extra['mac_address']) + + def test_ex_delete_network_interface(self): + interface = self.driver.ex_list_network_interfaces()[0] + resp = self.driver.ex_delete_network_interface(interface) + self.assertTrue(resp) + + def test_ex_attach_network_interface_to_node(self): + node = self.driver.list_nodes()[0] + interface = self.driver.ex_list_network_interfaces()[0] + resp = self.driver.ex_attach_network_interface_to_node(interface, + node, 1) + self.assertTrue(resp) + + def test_ex_detach_network_interface(self): + resp = self.driver.ex_detach_network_interface('eni-attach-2b588b47') + self.assertTrue(resp) + class EC2USWest1Tests(EC2Tests): region = 'us-west-1' @@ -1183,6 +1225,26 @@ class EC2MockHttp(MockHttpTestCase): body = self.fixtures.load('get_console_output.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _DescribeNetworkInterfaces(self, method, url, body, headers): + body = self.fixtures.load('describe_network_interfaces.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _CreateNetworkInterface(self, method, url, body, headers): + body = self.fixtures.load('create_network_interface.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DeleteNetworkInterface(self, method, url, body, headers): + body = self.fixtures.load('delete_network_interface.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _AttachNetworkInterface(self, method, url, body, headers): + body = self.fixtures.load('attach_network_interface.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DetachNetworkInterface(self, method, url, body, headers): + body = self.fixtures.load('detach_network_interface.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + class EucMockHttp(EC2MockHttp): fixtures = ComputeFileFixtures('ec2')
