Add live migration with trunk test

Change-Id: I2d75fae81145b4bd1c0d38fabd785bc26835be15
Related-Bug: #1914747
Depends-On: https://review.opendev.org/c/openstack/neutron/+/774245
Depends-On: https://review.opendev.org/c/openstack/nova/+/775838
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 52ccea7..73b795f 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -23,6 +23,8 @@
 from tempest.common import utils
 from tempest.common import waiters
 from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
 CONF = config.CONF
@@ -55,6 +57,10 @@
     def setup_clients(cls):
         super(LiveMigrationTestBase, cls).setup_clients()
         cls.admin_migration_client = cls.os_admin.migrations_client
+        cls.networks_client = cls.os_primary.networks_client
+        cls.subnets_client = cls.os_primary.subnets_client
+        cls.ports_client = cls.os_primary.ports_client
+        cls.trunks_client = cls.os_primary.trunks_client
 
     def _migrate_server_to(self, server_id, dest_host, volume_backed=False):
         kwargs = dict()
@@ -197,6 +203,86 @@
 
         self.assertEqual(volume_id1, volume_id2)
 
+    def _create_net_subnet(self, name, cidr):
+        net_name = data_utils.rand_name(name=name)
+        net = self.networks_client.create_network(name=net_name)['network']
+        self.addClassResourceCleanup(
+            self.networks_client.delete_network, net['id'])
+
+        subnet = self.subnets_client.create_subnet(
+            network_id=net['id'],
+            cidr=cidr,
+            ip_version=4)
+        self.addClassResourceCleanup(self.subnets_client.delete_subnet,
+                                     subnet['subnet']['id'])
+        return net
+
+    def _create_port(self, network_id, name):
+        name = data_utils.rand_name(name=name)
+        port = self.ports_client.create_port(name=name,
+                                             network_id=network_id)['port']
+        self.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+                                     self.ports_client.delete_port,
+                                     port_id=port['id'])
+        return port
+
+    def _create_trunk_with_subport(self):
+        tenant_network = self.get_tenant_network()
+        parent = self._create_port(network_id=tenant_network['id'],
+                                   name='parent')
+        net = self._create_net_subnet(name='subport_net', cidr='19.80.0.0/24')
+        subport = self._create_port(network_id=net['id'], name='subport')
+
+        trunk = self.trunks_client.create_trunk(
+            name=data_utils.rand_name('trunk'),
+            port_id=parent['id'],
+            sub_ports=[{"segmentation_id": 42, "port_id": subport['id'],
+                        "segmentation_type": "vlan"}]
+        )['trunk']
+        self.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+                                     self.trunks_client.delete_trunk,
+                                     trunk['id'])
+        return trunk, parent, subport
+
+    def _is_port_status_active(self, port_id):
+        port = self.ports_client.show_port(port_id)['port']
+        return port['status'] == 'ACTIVE'
+
+    @decorators.idempotent_id('0022c12e-a482-42b0-be2d-396b5f0cffe3')
+    @utils.requires_ext(service='network', extension='trunk')
+    @utils.services('network')
+    def test_live_migration_with_trunk(self):
+        """Test live migration with trunk and subport"""
+        trunk, parent, subport = self._create_trunk_with_subport()
+
+        server = self.create_test_server(
+            wait_until="ACTIVE", networks=[{'port': parent['id']}])
+
+        # Wait till subport status is ACTIVE
+        self.assertTrue(
+            test_utils.call_until_true(
+                self._is_port_status_active, CONF.validation.connect_timeout,
+                5, subport['id']))
+        parent = self.ports_client.show_port(parent['id'])['port']
+        self.assertEqual('ACTIVE', parent['status'])
+        subport = self.ports_client.show_port(subport['id'])['port']
+
+        if not CONF.compute_feature_enabled.can_migrate_between_any_hosts:
+            # not to specify a host so that the scheduler will pick one
+            target_host = None
+        else:
+            target_host = self.get_host_other_than(server['id'])
+
+        self._live_migrate(server['id'], target_host, 'ACTIVE')
+
+        # Wait till subport status is ACTIVE
+        self.assertTrue(
+            test_utils.call_until_true(
+                self._is_port_status_active, CONF.validation.connect_timeout,
+                5, subport['id']))
+        parent = self.ports_client.show_port(parent['id'])['port']
+        self.assertEqual('ACTIVE', parent['status'])
+
 
 class LiveMigrationRemoteConsolesV26Test(LiveMigrationTestBase):
     min_microversion = '2.6'
diff --git a/tempest/clients.py b/tempest/clients.py
index c4e00fe..6807fc4 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -72,6 +72,7 @@
         self.qos_client = self.network.QosClient()
         self.qos_min_bw_client = self.network.QosMinimumBandwidthRulesClient()
         self.segments_client = self.network.SegmentsClient()
+        self.trunks_client = self.network.TrunksClient()
 
     def _set_image_clients(self):
         if CONF.service_available.glance:
diff --git a/tempest/lib/services/network/__init__.py b/tempest/lib/services/network/__init__.py
index f7ac046..7e57499 100644
--- a/tempest/lib/services/network/__init__.py
+++ b/tempest/lib/services/network/__init__.py
@@ -36,6 +36,7 @@
 from tempest.lib.services.network.subnetpools_client import SubnetpoolsClient
 from tempest.lib.services.network.subnets_client import SubnetsClient
 from tempest.lib.services.network.tags_client import TagsClient
+from tempest.lib.services.network.trunks_client import TrunksClient
 from tempest.lib.services.network.versions_client import NetworkVersionsClient
 
 __all__ = ['AgentsClient', 'ExtensionsClient', 'FloatingIPsClient',
@@ -44,4 +45,4 @@
            'QosClient', 'QosMinimumBandwidthRulesClient', 'QuotasClient',
            'RoutersClient', 'SecurityGroupRulesClient', 'SecurityGroupsClient',
            'SegmentsClient', 'ServiceProvidersClient', 'SubnetpoolsClient',
-           'SubnetsClient', 'TagsClient']
+           'SubnetsClient', 'TagsClient', 'TrunksClient']
diff --git a/tempest/lib/services/network/trunks_client.py b/tempest/lib/services/network/trunks_client.py
new file mode 100644
index 0000000..2fd9e01
--- /dev/null
+++ b/tempest/lib/services/network/trunks_client.py
@@ -0,0 +1,100 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.services.network import base
+
+
+class TrunksClient(base.BaseNetworkClient):
+
+    def create_trunk(self, **kwargs):
+        """Creates a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#create-trunk
+        """
+        uri = '/trunks'
+        post_data = {'trunk': kwargs}
+        return self.create_resource(uri, post_data)
+
+    def update_trunk(self, trunk_id, **kwargs):
+        """Updates a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#update-trunk
+        """
+        uri = '/trunks/%s' % trunk_id
+        put_data = {'trunk': kwargs}
+        return self.update_resource(uri, put_data)
+
+    def show_trunk(self, trunk_id):
+        """Shows details for a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#show-trunk
+        """
+        uri = '/trunks/%s' % trunk_id
+        return self.show_resource(uri)
+
+    def delete_trunk(self, trunk_id):
+        """Deletes a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#delete-trunk
+        """
+        uri = '/trunks/%s' % trunk_id
+        return self.delete_resource(uri)
+
+    def list_trunks(self, **filters):
+        """Lists trunks to which the tenant has access.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#list-trunks
+        """
+        uri = '/trunks'
+        return self.list_resources(uri, **filters)
+
+    def add_subports_to_trunk(self, trunk_id, sub_ports):
+        """Add subports to a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#add-subports-to-trunk
+        """
+        uri = '/trunks/%s/add_subports' % trunk_id
+        put_data = {'sub_ports': sub_ports}
+        return self.update_resource(uri, put_data)
+
+    def delete_subports_from_trunk(self, trunk_id, sub_ports):
+        """Deletes subports from a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#delete-subports-from-trunk
+        """
+        uri = '/trunks/%s/remove_subports' % trunk_id
+        put_data = {'sub_ports': sub_ports}
+        return self.update_resource(uri, put_data)
+
+    def list_subports_of_trunk(self, trunk_id):
+        """List subports of a trunk.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/network/v2/index.html#list-subports-for-trunk
+        """
+        uri = '/trunks/%s/get_subports' % trunk_id
+        return self.list_resources(uri)
diff --git a/tempest/tests/lib/services/network/test_trunks_client.py b/tempest/tests/lib/services/network/test_trunks_client.py
new file mode 100644
index 0000000..b637d5e
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_trunks_client.py
@@ -0,0 +1,201 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import copy
+
+from tempest.lib.services.network import trunks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTrunksClient(base.BaseServiceTest):
+
+    FAKE_TRUNK_ID = "dfbc2103-93cf-4edf-952a-ef6deb32ddc6"
+    FAKE_PORT_ID = "1f04eb36-6c84-11eb-b0ab-4fc62961629d"
+    FAKE_TRUNKS = {
+        "trunks": [
+            {
+                "admin_state_up": True,
+                "description": "",
+                "id": "dfbc2103-93cf-4edf-952a-ef6deb32ddc6",
+                "name": "trunk0",
+                "port_id": "00130aab-bb51-42a1-a7c4-6703a3a43aa5",
+                "project_id": "",
+                "revision_number": 2,
+                "status": "DOWN",
+                "sub_ports": [
+                    {
+                        "port_id": "87d2483d-e5e6-483d-b5f0-81b9ed1d1a91",
+                        "segmentation_id": 101,
+                        "segmentation_type": "vlan"
+                        }
+                    ],
+                "tags": [],
+            },
+            {
+                "admin_state_up": True,
+                "description": "",
+                "id": "9eb0e72e-11d3-4295-bcaf-6c89008d9f0a",
+                "name": "trunk1",
+                "port_id": "035a12bf-2ae3-42ae-8ad6-9f70640cddde",
+                "project_id": "",
+                "revision_number": 2,
+                "status": "DOWN",
+                "sub_ports": [
+                    {
+                        "port_id": "cba839d5-02e2-4e09-b964-81356da78165",
+                        "segmentation_id": 102,
+                        "segmentation_type": "vlan"
+                        }
+                    ],
+                "tags": [],
+            },
+        ]
+    }
+
+    FAKE_TRUNK_1 = {
+        "name": "trunk0",
+        "port_id": "00130aab-bb51-42a1-a7c4-6703a3a43aa5"
+    }
+
+    def setUp(self):
+        super(TestTrunksClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.trunks_client = trunks_client.TrunksClient(
+            fake_auth, "network", "regionOne")
+
+    def _test_create_trunk(self, bytes_body=False):
+        self.check_service_client_function(
+            self.trunks_client.create_trunk,
+            "tempest.lib.common.rest_client.RestClient.post",
+            {"trunk": self.FAKE_TRUNKS["trunks"][0]},
+            bytes_body,
+            201,
+            **self.FAKE_TRUNK_1)
+
+    def _test_list_trunks(self, bytes_body=False):
+        self.check_service_client_function(
+            self.trunks_client.list_trunks,
+            "tempest.lib.common.rest_client.RestClient.get",
+            self.FAKE_TRUNKS,
+            bytes_body,
+            200)
+
+    def _test_show_trunk(self, bytes_body=False):
+        self.check_service_client_function(
+            self.trunks_client.show_trunk,
+            "tempest.lib.common.rest_client.RestClient.get",
+            {"trunk": self.FAKE_TRUNKS["trunks"][0]},
+            bytes_body,
+            200,
+            trunk_id=self.FAKE_TRUNK_ID)
+
+    def _test_update_trunk(self, bytes_body=False):
+        update_kwargs = {
+            "admin_state_up": True,
+            "name": "new_trunk"
+        }
+
+        resp_body = {
+            "trunk": copy.deepcopy(
+                self.FAKE_TRUNKS["trunks"][0]
+            )
+        }
+        resp_body["trunk"].update(update_kwargs)
+
+        self.check_service_client_function(
+            self.trunks_client.update_trunk,
+            "tempest.lib.common.rest_client.RestClient.put",
+            resp_body,
+            bytes_body,
+            200,
+            trunk_id=self.FAKE_TRUNK_ID,
+            **update_kwargs)
+
+    def _test_add_subports_to_trunk(self, bytes_body=False):
+        sub_ports = [{
+            "port_id": "f04eb36-6c84-11eb-b0ab-4fc62961629d",
+            "segmentation_type": "vlan",
+            "segmentation_id": "1001"
+        }]
+        resp_body = copy.deepcopy(self.FAKE_TRUNKS["trunks"][0])
+
+        resp_body["sub_ports"].append(sub_ports)
+        self.check_service_client_function(
+            self.trunks_client.add_subports_to_trunk,
+            "tempest.lib.common.rest_client.RestClient.put",
+            resp_body,
+            bytes_body,
+            200,
+            trunk_id=self.FAKE_TRUNK_ID,
+            sub_ports=sub_ports)
+
+    def _test_delete_subports_from_trunk(self, bytes_body=False):
+        fake_sub_ports = self.FAKE_TRUNKS['trunks'][0]['sub_ports']
+        sub_ports = [
+            {"port_id": fake_sub_ports[0]['port_id']}
+        ]
+        resp_body = copy.deepcopy(self.FAKE_TRUNKS["trunks"][0])
+
+        resp_body['sub_ports'] = []
+        self.check_service_client_function(
+            self.trunks_client.delete_subports_from_trunk,
+            "tempest.lib.common.rest_client.RestClient.put",
+            resp_body,
+            bytes_body,
+            200,
+            trunk_id=self.FAKE_TRUNK_ID,
+            sub_ports=sub_ports)
+
+    def test_create_trunk_with_str_body(self):
+        self._test_create_trunk()
+
+    def test_create_trunk_with_bytes_body(self):
+        self._test_create_trunk(bytes_body=True)
+
+    def test_list_trunks_with_str_body(self):
+        self._test_list_trunks()
+
+    def test_list_trunks_with_bytes_body(self):
+        self._test_list_trunks(bytes_body=True)
+
+    def test_show_trunk_with_str_body(self):
+        self._test_show_trunk()
+
+    def test_show_trunk_with_bytes_body(self):
+        self._test_show_trunk(bytes_body=True)
+
+    def test_update_trunk_with_str_body(self):
+        self._test_update_trunk()
+
+    def test_update_trunk_with_bytes_body(self):
+        self._test_update_trunk(bytes_body=True)
+
+    def test_add_subports_to_trunk_str_body(self):
+        self._test_add_subports_to_trunk()
+
+    def test_add_subports_to_trunk_bytes_body(self):
+        self._test_add_subports_to_trunk(bytes_body=True)
+
+    def test_delete_subports_from_trunk_str_body(self):
+        self._test_delete_subports_from_trunk()
+
+    def test_delete_subports_from_trunk_bytes_body(self):
+        self._test_delete_subports_from_trunk(bytes_body=True)
+
+    def test_delete_trunk(self):
+        self.check_service_client_function(
+            self.trunks_client.delete_trunk,
+            "tempest.lib.common.rest_client.RestClient.delete",
+            {},
+            status=204,
+            trunk_id=self.FAKE_TRUNK_ID)
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index 5a14430..b83eb34 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -302,6 +302,7 @@
       devstack_services:
         neutron-placement: true
         neutron-qos: true
+        neutron-trunk: true
     group-vars:
       subnode:
         devstack_localrc: