Add tempest tests for physical networks

This change adds tests to the ironic tempest plugin to cover the
API changes made for the physical network awareness feature in
I7023a1d6618608c867c31396fa677d3016ca493e.

Change-Id: I8b30764d797f2f8b45c2ae46ce559e74e0281a49
Partial-Bug: #1666009
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 40aa1d0..0a35a3d 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -195,6 +195,10 @@
         :param address: MAC address of the port.
         :param extra: Meta data of the port. Default: {'foo': 'bar'}.
         :param uuid: UUID of the port.
+        :param portgroup_uuid: The UUID of a portgroup of which this port is a
+            member.
+        :param physical_network: The physical network to which the port is
+            attached.
         :return: A tuple with the server response and the created port.
 
         """
@@ -204,8 +208,9 @@
         if node_id is not None:
             port['node_uuid'] = node_id
 
-        if kwargs['address'] is not None:
-            port['address'] = kwargs['address']
+        for key in ('address', 'physical_network', 'portgroup_uuid'):
+            if kwargs.get(key) is not None:
+                port[key] = kwargs[key]
 
         return self._create_request('ports', port)
 
diff --git a/ironic_tempest_plugin/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py
index 9577f83..ff51c86 100644
--- a/ironic_tempest_plugin/tests/api/admin/base.py
+++ b/ironic_tempest_plugin/tests/api/admin/base.py
@@ -185,7 +185,8 @@
 
     @classmethod
     @creates('port')
-    def create_port(cls, node_id, address, extra=None, uuid=None):
+    def create_port(cls, node_id, address, extra=None, uuid=None,
+                    portgroup_uuid=None, physical_network=None):
         """Wrapper utility for creating test ports.
 
         :param node_id: The unique identifier of the node.
@@ -193,12 +194,18 @@
         :param extra: Meta data of the port. If not supplied, an empty
             dictionary will be created.
         :param uuid: UUID of the port.
+        :param portgroup_uuid: The UUID of a portgroup of which this port is a
+            member.
+        :param physical_network: The physical network to which the port is
+            attached.
         :return: A tuple with the server response and the created port.
 
         """
         extra = extra or {}
         resp, body = cls.client.create_port(address=address, node_id=node_id,
-                                            extra=extra, uuid=uuid)
+                                            extra=extra, uuid=uuid,
+                                            portgroup_uuid=portgroup_uuid,
+                                            physical_network=physical_network)
 
         return resp, body
 
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports.py b/ironic_tempest_plugin/tests/api/admin/test_ports.py
index fe10573..a4aea4f 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports.py
@@ -14,6 +14,7 @@
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
 from ironic_tempest_plugin.tests.api.admin import base
 
 
@@ -256,3 +257,120 @@
         _, body = self.client.show_port(port['uuid'])
         self.assertEqual(new_address, body['address'])
         self.assertEqual(new_extra, body['extra'])
+
+
+class TestPortsWithPhysicalNetwork(base.BaseBaremetalTest):
+    """Tests for ports with physical network information."""
+
+    min_microversion = '1.34'
+
+    def setUp(self):
+        super(TestPortsWithPhysicalNetwork, self).setUp()
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture(
+                TestPortsWithPhysicalNetwork.min_microversion)
+        )
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id('f1a5d279-c456-4311-ad31-fea09f61c22b')
+    def test_create_port_with_physical_network(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, port = self.create_port(node_id=node_id, address=address,
+                                   physical_network='physnet1')
+
+        _, body = self.client.show_port(port['uuid'])
+
+        self._assertExpected(port, body)
+        self.assertEqual('physnet1', port['physical_network'])
+
+    @decorators.idempotent_id('9c26298b-1bcb-47b7-9b9e-8bdd6e3c4aba')
+    def test_update_port_replace_physical_network(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, port = self.create_port(node_id=node_id, address=address,
+                                   physical_network='physnet1')
+
+        new_physnet = 'physnet2'
+
+        patch = [{'path': '/physical_network',
+                  'op': 'replace',
+                  'value': new_physnet}]
+
+        self.client.update_port(port['uuid'], patch)
+
+        _, body = self.client.show_port(port['uuid'])
+        self.assertEqual(new_physnet, body['physical_network'])
+
+    @decorators.idempotent_id('6503309c-b2c7-4f59-b15a-0d92b5de9210')
+    def test_update_port_remove_physical_network(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, port = self.create_port(node_id=node_id, address=address,
+                                   physical_network='physnet1')
+
+        patch = [{'path': '/physical_network',
+                  'op': 'remove'}]
+
+        self.client.update_port(port['uuid'], patch)
+
+        _, body = self.client.show_port(port['uuid'])
+        self.assertIsNone(body['physical_network'])
+
+    @decorators.idempotent_id('4155c24d-8474-4b53-a320-aee475f85a68')
+    def test_create_ports_in_portgroup_with_physical_network(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, portgroup = self.create_portgroup(node_id, address=address)
+
+        _, port1 = self.create_port(node_id=node_id, address=address,
+                                    portgroup_uuid=portgroup['uuid'],
+                                    physical_network='physnet1')
+
+        address = data_utils.rand_mac_address()
+        _, port2 = self.create_port(node_id=node_id, address=address,
+                                    portgroup_uuid=portgroup['uuid'],
+                                    physical_network='physnet1')
+
+        _, body = self.client.show_port(port1['uuid'])
+        self.assertEqual('physnet1', body['physical_network'])
+        self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
+
+        _, body = self.client.show_port(port2['uuid'])
+        self.assertEqual('physnet1', body['physical_network'])
+        self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
+
+    @decorators.idempotent_id('cf05a3ef-3bc4-4db7-bb4c-4eb871eb9f81')
+    def test_update_ports_in_portgroup_with_physical_network(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, portgroup = self.create_portgroup(node_id, address=address)
+
+        _, port1 = self.create_port(node_id=node_id, address=address,
+                                    portgroup_uuid=portgroup['uuid'],
+                                    physical_network='physnet1')
+
+        address = data_utils.rand_mac_address()
+        _, port2 = self.create_port(node_id=node_id, address=address,
+                                    physical_network='physnet1')
+
+        patch = [{'path': '/portgroup_uuid',
+                  'op': 'replace',
+                  'value': portgroup['uuid']}]
+
+        self.client.update_port(port2['uuid'], patch)
+
+        _, body = self.client.show_port(port1['uuid'])
+        self.assertEqual('physnet1', body['physical_network'])
+        self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
+
+        _, body = self.client.show_port(port2['uuid'])
+        self.assertEqual('physnet1', body['physical_network'])
+        self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
index 91ba7fd..86a0ce6 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
@@ -14,6 +14,7 @@
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
 from ironic_tempest_plugin.tests.api.admin import base
 
 
@@ -337,3 +338,128 @@
         _, body = self.client.show_port(port_id)
         self.assertEqual(address, body['address'])
         self.assertEqual(extra, body['extra'])
+
+
+class TestPortsWithPhysicalNetworkOldAPI(base.BaseBaremetalTest):
+    """Negative tests for ports with physical network information."""
+
+    old_microversion = '1.33'
+
+    def setUp(self):
+        super(TestPortsWithPhysicalNetworkOldAPI, self).setUp()
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture(
+                TestPortsWithPhysicalNetworkOldAPI.old_microversion)
+        )
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id('307e57e9-082f-4830-9480-91affcbfda08')
+    def test_create_port_with_physical_network_old_api(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        self.assertRaises(lib_exc.UnexpectedResponseCode,
+                          self.create_port,
+                          node_id=node_id, address=address,
+                          physical_network='physnet1')
+
+    @decorators.idempotent_id('0b278c0a-d334-424e-a5c5-b6d001c2a715')
+    def test_update_port_replace_physical_network_old_api(self):
+        _, port = self.create_port(self.node['uuid'],
+                                   data_utils.rand_mac_address())
+
+        new_physnet = 'physnet1'
+
+        patch = [{'path': '/physical_network',
+                  'op': 'replace',
+                  'value': new_physnet}]
+
+        self.assertRaises(lib_exc.UnexpectedResponseCode,
+                          self.client.update_port,
+                          port['uuid'], patch)
+
+
+class TestPortsNegativeWithPhysicalNetwork(base.BaseBaremetalTest):
+    """Negative tests for ports with physical network information."""
+
+    min_microversion = '1.34'
+
+    def setUp(self):
+        super(TestPortsNegativeWithPhysicalNetwork, self).setUp()
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture(
+                TestPortsNegativeWithPhysicalNetwork.min_microversion)
+        )
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id('e20156fb-956b-4d5b-89a4-f379044a1d3c')
+    def test_create_ports_in_portgroup_with_inconsistent_physical_network(
+            self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, portgroup = self.create_portgroup(node_id, address=address)
+
+        _, _ = self.create_port(node_id=node_id, address=address,
+                                portgroup_uuid=portgroup['uuid'],
+                                physical_network='physnet1')
+
+        address = data_utils.rand_mac_address()
+        self.assertRaises(lib_exc.Conflict,
+                          self.create_port,
+                          node_id=node_id, address=address,
+                          portgroup_uuid=portgroup['uuid'],
+                          physical_network='physnet2')
+
+    @decorators.idempotent_id('050e792c-22c9-4e4a-ae89-dfbfc52ad00d')
+    def test_update_ports_in_portgroup_with_inconsistent_physical_network(
+            self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, portgroup = self.create_portgroup(node_id, address=address)
+
+        _, _ = self.create_port(node_id=node_id, address=address,
+                                portgroup_uuid=portgroup['uuid'],
+                                physical_network='physnet1')
+
+        address = data_utils.rand_mac_address()
+        _, port2 = self.create_port(node_id=node_id, address=address,
+                                    physical_network='physnet2')
+
+        patch = [{'path': '/portgroup_uuid',
+                  'op': 'replace',
+                  'value': portgroup['uuid']}]
+
+        self.assertRaises(lib_exc.Conflict,
+                          self.client.update_port,
+                          port2['uuid'], patch)
+
+    @decorators.idempotent_id('3cd1c8ec-57d1-40cb-922b-dd02431beea3')
+    def test_update_ports_in_portgroup_with_inconsistent_physical_network_2(
+            self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        _, portgroup = self.create_portgroup(node_id, address=address)
+
+        _, _ = self.create_port(node_id=node_id, address=address,
+                                portgroup_uuid=portgroup['uuid'],
+                                physical_network='physnet1')
+
+        address = data_utils.rand_mac_address()
+        _, port2 = self.create_port(node_id=node_id, address=address,
+                                    portgroup_uuid=portgroup['uuid'],
+                                    physical_network='physnet1')
+
+        patch = [{'path': '/physical_network',
+                  'op': 'replace',
+                  'value': 'physnet2'}]
+
+        self.assertRaises(lib_exc.Conflict,
+                          self.client.update_port,
+                          port2['uuid'], patch)