libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From quent...@apache.org
Subject [1/2] libcloud git commit: implement OpenStackv2 port attaching/detaching
Date Fri, 06 Jul 2018 06:12:30 GMT
Repository: libcloud
Updated Branches:
  refs/heads/trunk 1ac9467a9 -> b2b7684a7


implement OpenStackv2 port attaching/detaching

Adds Port Interface calls for the OpenStack v2 driver. Similar in
functionality to a floating IPs (can be attached / detached from a
compute instance to move IPs between instances) but a bit different.

> A port is a connection point for attaching a single device, such as the
> NIC of a server, to a network. The port also describes the associated
> network configuration, such as the MAC and IP addresses to be used on
> that port.
https://docs.openstack.org/python-openstackclient/pike/cli/command-objects/port.html

Also see:
https://developer.openstack.org/api-ref/compute/#port-interfaces-servers-os-interface

This commit adds:
- a connection to the neutron network API for functionality that is not exposed through
the nova compute api
- an OpenStack_2_PortInterface object
- an OpenStack_2_PortInterfaceState object for port interface states
- an ex_list_ports method (via the neutron api)
- an ex_delete_port method (via the neutron api)
- an ex_detach_port_interface method (via the nova api)
- an ex_attach_port_interface method (via the nova api)

Signed-off-by: Quentin Pradet <quentinp@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/c24c71fd
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/c24c71fd
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/c24c71fd

Branch: refs/heads/trunk
Commit: c24c71fd3265270cc2df0ab08f53239d7a06063c
Parents: 1ac9467
Author: Rick van de Loo <rickvandeloo@gmail.com>
Authored: Thu Jul 5 17:18:27 2018 +0200
Committer: Quentin Pradet <quentinp@apache.org>
Committed: Fri Jul 6 10:02:23 2018 +0400

----------------------------------------------------------------------
 libcloud/compute/drivers/openstack.py           | 209 ++++++++++++++++++-
 libcloud/test/common/test_openstack_identity.py |   7 +-
 .../compute/fixtures/openstack/_v2_0__auth.json |  22 ++
 .../fixtures/openstack_v1.1/_ports_v2.json      | 185 ++++++++++++++++
 libcloud/test/compute/test_openstack.py         |  83 +++++++-
 5 files changed, 499 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/c24c71fd/libcloud/compute/drivers/openstack.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack.py
index 5307376..e0a5ff0 100644
--- a/libcloud/compute/drivers/openstack.py
+++ b/libcloud/compute/drivers/openstack.py
@@ -38,12 +38,13 @@ from libcloud.common.openstack import OpenStackDriverMixin
 from libcloud.common.openstack import OpenStackException
 from libcloud.common.openstack import OpenStackResponse
 from libcloud.utils.networking import is_public_subnet
-from libcloud.compute.base import NodeSize, NodeImage, NodeImageMember
+from libcloud.compute.base import NodeSize, NodeImage, NodeImageMember, \
+    UuidMixin
 from libcloud.compute.base import (NodeDriver, Node, NodeLocation,
                                    StorageVolume, VolumeSnapshot)
 from libcloud.compute.base import KeyPair
 from libcloud.compute.types import NodeState, StorageVolumeState, Provider, \
-    VolumeSnapshotState
+    VolumeSnapshotState, Type
 from libcloud.pricing import get_size_price
 from libcloud.utils.xml import findall
 from libcloud.utils.py3 import ET
@@ -59,6 +60,8 @@ __all__ = [
     'OpenStack_1_1_NodeDriver',
     'OpenStack_1_1_FloatingIpPool',
     'OpenStack_1_1_FloatingIpAddress',
+    'OpenStack_2_PortInterfaceState',
+    'OpenStack_2_PortInterface',
     'OpenStackNodeDriver'
 ]
 
@@ -80,6 +83,12 @@ class OpenStackImageConnection(OpenStackBaseConnection):
     service_region = 'RegionOne'
 
 
+class OpenStackNetworkConnection(OpenStackBaseConnection):
+    service_type = 'network'
+    service_name = 'neutron'
+    service_region = 'RegionOne'
+
+
 class OpenStackNodeDriver(NodeDriver, OpenStackDriverMixin):
     """
     Base OpenStack node driver. Should not be used directly.
@@ -2491,6 +2500,25 @@ class OpenStack_2_ImageConnection(OpenStackImageConnection):
         return json.dumps(data)
 
 
+class OpenStack_2_NetworkConnection(OpenStackNetworkConnection):
+    responseCls = OpenStack_1_1_Response
+    accept_format = 'application/json'
+    default_content_type = 'application/json; charset=UTF-8'
+
+    def encode_data(self, data):
+        return json.dumps(data)
+
+
+class OpenStack_2_PortInterfaceState(Type):
+    """
+    Standard states of OpenStack_2_PortInterfaceState
+    """
+    BUILD = 'build'
+    ACTIVE = 'active'
+    DOWN = 'down'
+    UNKNOWN = 'unknown'
+
+
 class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver):
     """
     OpenStack node driver.
@@ -2514,11 +2542,33 @@ class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver):
     # image/v2/index.html#list-image-members
     image_connectionCls = OpenStack_2_ImageConnection
     image_connection = None
+
+    # Similarly not all node-related operations are exposed through the
+    # compute API
+    # See https://developer.openstack.org/api-ref/compute/
+    # For example, creating a new node in an OpenStack that is configured to
+    # create a new port for every new instance will make it so that if that
+    # port is detached it disappears. But if the port is manually created
+    # beforehand using the neutron network API and node is booted with that
+    # port pre-specified, then detaching that port later will result in that
+    # becoming a re-attachable resource much like a floating ip. So because
+    # even though this is the compute driver, we do connect to the networking
+    # API here because some operations relevant for compute can only be
+    # accessed from there.
+    network_connectionCls = OpenStack_2_NetworkConnection
+    network_connection = None
     type = Provider.OPENSTACK
 
     features = {"create_node": ["generates_password"]}
     _networks_url_prefix = '/os-networks'
 
+    PORT_INTERFACE_MAP = {
+        'BUILD': OpenStack_2_PortInterfaceState.BUILD,
+        'ACTIVE': OpenStack_2_PortInterfaceState.ACTIVE,
+        'DOWN': OpenStack_2_PortInterfaceState.DOWN,
+        'UNKNOWN': OpenStack_2_PortInterfaceState.UNKNOWN
+    }
+
     def __init__(self, *args, **kwargs):
         original_connectionCls = self.connectionCls
         self._ex_force_api_version = str(kwargs.pop('ex_force_api_version',
@@ -2532,11 +2582,47 @@ class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver):
         super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs)
         self.image_connection = self.connection
 
-        # We run the init again to get the compute API connection
+        # We run the init again to get the Neutron V2 API connection
+        # and put that on the object under self.network_connection.
+        self.connectionCls = self.network_connectionCls
+        super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs)
+        self.network_connection = self.connection
+
+        # We run the init once again to get the compute API connection
         # and that's put under self.connection as normal.
         self.connectionCls = original_connectionCls
         super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs)
 
+    def _to_port(self, element):
+        created = element['created_at']
+        updated = element.get('updated_at')
+        return OpenStack_2_PortInterface(
+            id=element['id'],
+            state=self.PORT_INTERFACE_MAP.get(
+                element.get('status'), OpenStack_2_PortInterfaceState.UNKNOWN
+            ),
+            created=created,
+            driver=self,
+            extra=dict(
+                allowed_address_pairs=element['allowed_address_pairs'],
+                binding_vnic_type=element['binding:vnic_type'],
+                device_id=element['device_id'],
+                description=element['description'],
+                device_owner=element['device_owner'],
+                fixed_ips=element['fixed_ips'],
+                mac_address=element['mac_address'],
+                name=element['name'],
+                network_id=element['network_id'],
+                project_id=element['project_id'],
+                port_security_enabled=element['port_security_enabled'],
+                revision_number=element['revision_number'],
+                security_groups=element['security_groups'],
+                tags=element['tags'],
+                tenant_id=element['tenant_id'],
+                updated=updated,
+            )
+        )
+
     def get_image(self, image_id):
         """
         Get a NodeImage using the V2 Glance API
@@ -2696,6 +2782,73 @@ class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver):
         )
         return self._to_image_member(response.object)
 
+    def ex_list_ports(self):
+        """
+        List all OpenStack_2_PortInterfaces
+
+        https://developer.openstack.org/api-ref/network/v2/#list-ports
+
+        :rtype: ``list`` of :class:`OpenStack_2_PortInterface`
+        """
+        response = self.network_connection.request(
+            '/v2.0/ports'
+        )
+        return [self._to_port(port) for port in response.object['ports']]
+
+    def ex_delete_port(self, port):
+        """
+        Delete an OpenStack_2_PortInterface
+
+        https://developer.openstack.org/api-ref/network/v2/#delete-port
+
+        :param      port: port interface to remove
+        :type       port: :class:`OpenStack_2_PortInterface`
+
+        :rtype: ``bool``
+         """
+        response = self.network_connection.request(
+            '/v2.0/ports/%s' % port.id, method='DELETE'
+        )
+        return response.success()
+
+    def ex_detach_port_interface(self, node, port):
+        """
+        Detaches an OpenStack_2_PortInterface interface from a Node.
+        :param      node: node
+        :type       node: :class:`Node`
+
+        :param      port: port interface to remove
+        :type       port: :class:`OpenStack_2_PortInterface`
+
+        :rtype: ``bool``
+        """
+        return self.connection.request(
+            '/servers/%s/os-interface/%s' % (node.id, port.id),
+            method='DELETE'
+        ).success()
+
+    def ex_attach_port_interface(self, node, port):
+        """
+        Attaches an OpenStack_2_PortInterface to a Node.
+
+        :param      node: node
+        :type       node: :class:`Node`
+
+        :param      port: port interface to remove
+        :type       port: :class:`OpenStack_2_PortInterface`
+
+        :rtype: ``bool``
+        """
+        data = {
+            'interfaceAttachment': {
+                'port_id': port.id
+            }
+        }
+        return self.connection.request(
+            '/servers/{}/os-interface'.format(node.id),
+            method='POST', data=data
+        ).success()
+
 
 class OpenStack_1_1_FloatingIpPool(object):
     """
@@ -2800,3 +2953,53 @@ class OpenStack_1_1_FloatingIpAddress(object):
         return ('<OpenStack_1_1_FloatingIpAddress: id=%s, ip_addr=%s,'
                 ' pool=%s, driver=%s>'
                 % (self.id, self.ip_address, self.pool, self.driver))
+
+
+class OpenStack_2_PortInterface(UuidMixin):
+    """
+    Port Interface info. Similar in functionality to a floating IP (can be
+    attached / detached from a compute instance) but implementation-wise a
+    bit different.
+
+    > A port is a connection point for attaching a single device, such as the
+    > NIC of a server, to a network. The port also describes the associated
+    > network configuration, such as the MAC and IP addresses to be used on
+    > that port.
+    https://docs.openstack.org/python-openstackclient/pike/cli/command-objects/port.html
+
+    Also see:
+    https://developer.openstack.org/api-ref/compute/#port-interfaces-servers-os-interface
+    """
+
+    def __init__(self, id, state, driver, created=None, extra=None):
+        """
+        :param id: Port Interface ID.
+        :type id: ``str``
+        :param state: State of the OpenStack_2_PortInterface.
+        :type state: :class:`.OpenStack_2_PortInterfaceState`
+        :param      created: A datetime object that represents when the
+                             port interface was created
+        :type       created: ``datetime.datetime``
+        :param extra: Optional provided specific attributes associated with
+                      this image.
+        :type extra: ``dict``
+        """
+        self.id = str(id)
+        self.state = state
+        self.driver = driver
+        self.created = created
+        self.extra = extra or {}
+        UuidMixin.__init__(self)
+
+    def delete(self):
+        """
+        Delete this Port Interface
+
+        :rtype: ``bool``
+        """
+        return self.driver.ex_delete_port(self)
+
+    def __repr__(self):
+        return (('<OpenStack_2_PortInterface: id=%s, state=%s, '
+                 'driver=%s  ...>')
+                % (self.id, self.state, self.driver.name))

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c24c71fd/libcloud/test/common/test_openstack_identity.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_openstack_identity.py b/libcloud/test/common/test_openstack_identity.py
index 9ac042c..c36f13a 100644
--- a/libcloud/test/common/test_openstack_identity.py
+++ b/libcloud/test/common/test_openstack_identity.py
@@ -525,7 +525,7 @@ class OpenStackServiceCatalogTestCase(unittest.TestCase):
         catalog = OpenStackServiceCatalog(service_catalog=service_catalog,
                                           auth_version='2.0')
         entries = catalog.get_entries()
-        self.assertEqual(len(entries), 7)
+        self.assertEqual(len(entries), 8)
 
         entry = [e for e in entries if e.service_name == 'cloudServers'][0]
         self.assertEqual(entry.service_type, 'compute')
@@ -591,8 +591,8 @@ class OpenStackServiceCatalogTestCase(unittest.TestCase):
         catalog = OpenStackServiceCatalog(service_catalog=service_catalog,
                                           auth_version='2.0')
         service_types = catalog.get_service_types()
-        self.assertEqual(service_types, ['compute', 'image', 'object-store',
-                                         'rax:object-cdn'])
+        self.assertEqual(service_types, ['compute', 'image', 'network',
+                                         'object-store', 'rax:object-cdn'])
 
         service_types = catalog.get_service_types(region='ORD')
         self.assertEqual(service_types, ['rax:object-cdn'])
@@ -611,6 +611,7 @@ class OpenStackServiceCatalogTestCase(unittest.TestCase):
                                          'cloudServersOpenStack',
                                          'cloudServersPreprod',
                                          'glance',
+                                         'neutron',
                                          'nova'])
 
         service_names = catalog.get_service_names(service_type='compute')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c24c71fd/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json b/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json
index 91fe35b..79c6776 100644
--- a/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json
+++ b/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json
@@ -101,6 +101,28 @@
             {
                 "endpoints": [
                     {
+                        "region": "RegionOne",
+                        "tenantId": "1337",
+                        "publicURL": "https://test_endpoint.com/v2/1337",
+                        "versionInfo": "https://test_endpoint.com/v2/",
+                        "versionList": "https://test_endpoint.com/",
+                        "versionId": "2"
+                    },
+                    {
+                        "region": "fr1",
+                        "tenantId": "1337",
+                        "publicURL": "https://test_endpoint.com/v2/1337",
+                        "versionInfo": "https://test_endpoint.com/v2/",
+                        "versionList": "https://test_endpoint.com/",
+                        "versionId": "2"
+                    }
+                ],
+                "name": "neutron",
+                "type": "network"
+            },
+            {
+                "endpoints": [
+                    {
                         "region": "DFW",
                         "tenantId": "613469",
                         "publicURL": "https://dfw.servers.api.rackspacecloud.com/v2/1337",

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c24c71fd/libcloud/test/compute/fixtures/openstack_v1.1/_ports_v2.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_ports_v2.json b/libcloud/test/compute/fixtures/openstack_v1.1/_ports_v2.json
new file mode 100644
index 0000000..e31f8f3
--- /dev/null
+++ b/libcloud/test/compute/fixtures/openstack_v1.1/_ports_v2.json
@@ -0,0 +1,185 @@
+{
+    "ports": [
+        {
+            "status": "BUILD", 
+            "extra_dhcp_opts": [], 
+            "description": "", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-04T14:38:18Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T14:40:43Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "compute:nova", 
+            "name": "", 
+            "revision_number": 2036, 
+            "mac_address": "ba:12:12:8a:b2:73", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.32"
+                }
+            ], 
+            "id": "126da55e-cfcb-41c8-ae39-a26cb8a7e723", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": "95e75643-2008-123f-ad13-e20ea64e3c87"
+        }, 
+        {
+            "status": "BUILD", 
+            "extra_dhcp_opts": [], 
+            "description": "porttest", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-05T12:38:50Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T14:40:43Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "compute:nova", 
+            "name": "porttest", 
+            "revision_number": 865, 
+            "mac_address": "ba:12:12:48:42:9b", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.31"
+                }
+            ], 
+            "id": "a8f3ddbe-9b29-41ac-9c0a-9ea7cc012dfb", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": "7b4743a6-f7f7-4764-9854-cf43312e6d49"
+        }, 
+        {
+            "status": "DOWN", 
+            "extra_dhcp_opts": [], 
+            "description": "", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-05T13:09:27Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T13:29:38Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "compute:nova", 
+            "name": "", 
+            "revision_number": 10, 
+            "mac_address": "ba:12:12:95:13:cc", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.44"
+                }
+            ], 
+            "id": "bad9af6a-121d-4772-9ae0-7d127b712f5d", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": "500d78d1-f84f-4949-12da-5205c1237121"
+        }, 
+        {
+            "status": "DOWN", 
+            "extra_dhcp_opts": [], 
+            "description": "testport", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-05T11:59:13Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T12:39:48Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "", 
+            "name": "testport", 
+            "revision_number": 32, 
+            "mac_address": "ba:12:12:6f:ea:12", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.12"
+                }
+            ], 
+            "id": "c21297ca-4e68-4384-badf-10903cd2cbb0", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": ""
+        }, 
+        {
+            "status": "DOWN", 
+            "extra_dhcp_opts": [], 
+            "description": "testport", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-05T11:56:59Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T11:57:00Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "", 
+            "name": "testport", 
+            "revision_number": 3, 
+            "mac_address": "ba:12:12:e6:03:ba", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.28"
+                }
+            ], 
+            "id": "ca335147-273c-4c72-9bab-11a122a95ce1", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": ""
+        }, 
+        {
+            "status": "DOWN", 
+            "extra_dhcp_opts": [], 
+            "description": "testport", 
+            "allowed_address_pairs": [], 
+            "tags": [], 
+            "network_id": "123c8a8c-6427-4e8f-a805-2035365f4d43", 
+            "tenant_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "created_at": "2018-07-05T11:12:17Z", 
+            "admin_state_up": true, 
+            "updated_at": "2018-07-05T11:12:17Z", 
+            "binding:vnic_type": "normal", 
+            "device_owner": "", 
+            "name": "testport", 
+            "revision_number": 3, 
+            "mac_address": "ba:12:12:12:12:12", 
+            "port_security_enabled": true, 
+            "project_id": "abcdec85bee34bb0a44ab8255eb36abc", 
+            "fixed_ips": [
+                {
+                    "subnet_id": "1231a12a-125b-4329-a3c5-312ea86a7577", 
+                    "ip_address": "12.123.12.12"
+                }
+            ], 
+            "id": "a128dec6-4f3a-45c4-a89c-678f69a72044", 
+            "security_groups": [
+                "abcfb112-5b5c-4c6b-8b3f-dbaee57df440"
+            ], 
+            "device_id": ""
+        }
+    ]
+}
+

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c24c71fd/libcloud/test/compute/test_openstack.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_openstack.py b/libcloud/test/compute/test_openstack.py
index 54606ef..19b75e7 100644
--- a/libcloud/test/compute/test_openstack.py
+++ b/libcloud/test/compute/test_openstack.py
@@ -47,7 +47,7 @@ from libcloud.compute.drivers.openstack import (
     OpenStack_1_1_FloatingIpAddress, OpenStackKeyPair,
     OpenStack_1_0_Connection,
     OpenStackNodeDriver,
-    OpenStack_2_NodeDriver)
+    OpenStack_2_NodeDriver, OpenStack_2_PortInterfaceState)
 from libcloud.compute.base import Node, NodeImage, NodeSize
 from libcloud.pricing import set_pricing, clear_pricing_data
 
@@ -1578,6 +1578,11 @@ class OpenStack_2_Tests(OpenStack_1_1_Tests):
         # normally authentication happens lazily, but we force it here
         self.driver.image_connection._populate_hosts_and_request_paths()
 
+        self.driver_klass.network_connectionCls.conn_class = OpenStack_2_0_MockHttp
+        self.driver_klass.network_connectionCls.auth_url = "https://auth.api.example.com"
+        # normally authentication happens lazily, but we force it here
+        self.driver.network_connection._populate_hosts_and_request_paths()
+
     def test_ex_force_auth_token_passed_to_connection(self):
         base_url = 'https://servers.api.rackspacecloud.com/v1.1/slug'
         kwargs = {
@@ -1692,6 +1697,57 @@ class OpenStack_2_Tests(OpenStack_1_1_Tests):
         self.assertEqual(image_member.extra['updated'], '2018-03-02T14:20:37Z')
         self.assertEqual(image_member.extra['schema'], '/v2/schemas/member')
 
+    def test_ex_list_ports(self):
+        ports = self.driver.ex_list_ports()
+
+        port = ports[0]
+        self.assertEqual(port.id, '126da55e-cfcb-41c8-ae39-a26cb8a7e723')
+        self.assertEqual(port.state, OpenStack_2_PortInterfaceState.BUILD)
+        self.assertEqual(port.created, '2018-07-04T14:38:18Z')
+        self.assertEqual(
+            port.extra['network_id'],
+            '123c8a8c-6427-4e8f-a805-2035365f4d43'
+        )
+        self.assertEqual(
+            port.extra['project_id'],
+            'abcdec85bee34bb0a44ab8255eb36abc'
+        )
+        self.assertEqual(
+            port.extra['tenant_id'],
+            'abcdec85bee34bb0a44ab8255eb36abc'
+        )
+        self.assertEqual(port.extra['name'], '')
+
+    def test_ex_delete_port(self):
+        ports = self.driver.ex_list_ports()
+        port = ports[0]
+
+        ret = self.driver.ex_delete_port(port)
+
+        self.assertTrue(ret)
+
+    def test_detach_port_interface(self):
+        node = Node(id='1c01300f-ef97-4937-8f03-ac676d6234be', name=None,
+                    state=None, public_ips=None, private_ips=None,
+                    driver=self.driver)
+        ports = self.driver.ex_list_ports()
+        port = ports[0]
+
+        ret = self.driver.ex_detach_port_interface(node, port)
+
+        self.assertTrue(ret)
+
+    def test_attach_port_interface(self):
+        node = Node(id='1c01300f-ef97-4937-8f03-ac676d6234be', name=None,
+                    state=None, public_ips=None, private_ips=None,
+                    driver=self.driver)
+        ports = self.driver.ex_list_ports()
+        port = ports[0]
+
+        ret = self.driver.ex_attach_port_interface(node, port)
+
+        self.assertTrue(ret)
+
 
 class OpenStack_1_1_FactoryMethodTests(OpenStack_1_1_Tests):
     should_list_locations = False
@@ -1905,6 +1961,31 @@ class OpenStack_1_1_MockHttp(MockHttp, unittest.TestCase):
         else:
             raise NotImplementedError()
 
+    def _v2_1337_v2_0_ports(self, method, url, body, headers):
+        if method == "GET":
+            body = self.fixtures.load('_ports_v2.json')
+            return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK])
+        else:
+            raise NotImplementedError()
+
+    def _v2_1337_v2_0_ports_126da55e_cfcb_41c8_ae39_a26cb8a7e723(self, method, url, body,
headers):
+        if method == "DELETE":
+            return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT])
+        else:
+            raise NotImplementedError()
+
+    def _v2_1337_servers_1c01300f_ef97_4937_8f03_ac676d6234be_os_interface_126da55e_cfcb_41c8_ae39_a26cb8a7e723(self,
method, url, body, headers):
+        if method == "DELETE":
+            return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT])
+        else:
+            raise NotImplementedError()
+
+    def _v2_1337_servers_1c01300f_ef97_4937_8f03_ac676d6234be_os_interface(self, method,
url, body, headers):
+        if method == "POST":
+            return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT])
+        else:
+            raise NotImplementedError()
+
     def _v1_1_slug_servers_1c01300f_ef97_4937_8f03_ac676d6234be_os_security_groups(self,
method, url, body, headers):
         if method == "GET":
             body = self.fixtures.load(


Mime
View raw message