Tap Mirror API and scenario tests

Change-Id: I0876068eb15053853f52ba9bdab1f6cce484f417
Depends-On: https://review.opendev.org/c/openstack/tap-as-a-service/+/893509
Depends-On: https://review.opendev.org/c/openstack/neutron/+/905840
Related-Bug: #2015471
diff --git a/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py b/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py
new file mode 100644
index 0000000..85c37c6
--- /dev/null
+++ b/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py
@@ -0,0 +1,218 @@
+#    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 oslo_utils import uuidutils
+
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from neutron_tempest_plugin.tap_as_a_service import base
+
+
+class TapMirrorTestJSON(base.BaseTaasTest):
+
+    @classmethod
+    @utils.requires_ext(extension='tap-mirror', service='network')
+    def skip_checks(cls):
+        super().skip_checks()
+
+    @classmethod
+    def resource_setup(cls):
+        super().resource_setup()
+        cls.network = cls.create_network()
+        cls.tap_mirror_port = cls.create_port(cls.network)
+        cls.in_direction = {'IN': 101}
+        cls.out_direction = {'OUT': 102}
+        cls.both_direction = cls.in_direction | cls.out_direction
+        cls.remote_ip = '192.101.0.42'
+        cls.remote_ip2 = '192.101.3.43'
+        cls.gre = 'gre'
+        cls.erspan = 'erspanv1'
+
+    @decorators.idempotent_id('628f202c-ed0a-4eb1-8547-4954f67a84b7')
+    def test_create_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.assertEqual(self.tap_mirror_port['id'], tap_mirror['port_id'])
+        self.assertEqual('gre', tap_mirror['mirror_type'])
+        self.assertEqual(self.in_direction, tap_mirror['directions'])
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror['id'])
+
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.both_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.erspan
+        )
+        self.assertEqual(self.tap_mirror_port['id'], tap_mirror['port_id'])
+        self.assertEqual(self.erspan, tap_mirror['mirror_type'])
+        self.assertEqual(self.both_direction, tap_mirror['directions'])
+
+    @decorators.idempotent_id('299c251b-e0bc-4449-98db-959a5d8038c2')
+    def test_list_show_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        tap_mirrors = self.tap_mirrors_client.list_tap_mirrors()
+        is_t_m_found = False
+        for t_m in tap_mirrors['tap_mirrors']:
+            if t_m['id'] == tap_mirror['id']:
+                is_t_m_found = True
+                break
+        self.assertTrue(is_t_m_found)
+        tap_mirror_show_res = self.tap_mirrors_client.show_tap_mirror(
+            tap_mirror['id'])['tap_mirror']
+        self.assertEqual(tap_mirror['id'], tap_mirror_show_res['id'])
+        self.assertEqual(self.gre, tap_mirror_show_res['mirror_type'])
+        self.assertEqual(self.remote_ip,
+                         tap_mirror_show_res['remote_ip'])
+        self.assertEqual(self.out_direction,
+                         tap_mirror_show_res['directions'])
+
+    @decorators.idempotent_id('19c40379-bda5-48c9-8873-fc990739d1b5')
+    def test_update_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.tap_mirrors_client.update_tap_mirror(
+            tap_mirror_id=tap_mirror['id'],
+            name='new_name',
+            description='My fancy Tap Mirror'
+        )
+        tap_mirror_show_res = self.tap_mirrors_client.show_tap_mirror(
+            tap_mirror['id'])['tap_mirror']
+        self.assertEqual('new_name', tap_mirror_show_res['name'])
+        self.assertEqual('My fancy Tap Mirror',
+                         tap_mirror_show_res['description'])
+
+    @decorators.idempotent_id('9ed165af-7c54-43ac-b14f-077e8f9601f6')
+    def test_delete_mirror_port_deletes_tap_mirror(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        # Delete port will result in deteltion of the tap_mirror
+        self.ports_client.delete_port(port1['id'])
+        self.assertRaises(lib_exc.NotFound,
+                          self.tap_mirrors_client.show_tap_mirror,
+                          tap_mirror['id'])
+
+    @decorators.idempotent_id('abdd4451-bd9d-4f1e-ab7f-e949b9246714')
+    def test_delete_tap_mirror_port_remains(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        # Delete tap_mirror will keep the port
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror['id'])
+        port_res = self.ports_client.show_port(port1['id'])['port']
+        self.assertEqual(port1['name'], port_res['name'])
+
+    @decorators.idempotent_id('1d8b68fc-a600-4b9e-bd17-9469c3a6c95b')
+    def test_create_tap_mirror_negative(self):
+        # directions keys' valid values are IN and OUT
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions={'something': 101},
+                          remote_ip=self.remote_ip,
+                          mirror_type=self.gre)
+        # mirror_type valid values are erspanv1 and gre
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions=self.out_direction,
+                          remote_ip=self.remote_ip,
+                          mirror_type='erspanv2')
+        # remote_ip must be a valid IP
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions=self.in_direction,
+                          remote_ip='192.101.0.420',
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('2b7850b3-3920-4f16-96b7-05e2efd96877')
+    def test_create_tap_service_tunnel_id_conflict(self):
+        self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+
+        port2 = self.create_port(self.network)
+        self.addCleanup(self.ports_client.delete_port, port2['id'])
+        self.assertRaises(lib_exc.Conflict,
+                          self.create_tap_mirror,
+                          port_id=port2['id'],
+                          directions=self.in_direction,
+                          remote_ip='192.101.0.4',
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('95ef1cc1-cd57-4193-a88e-716795e39ebf')
+    def test_create_tap_mirror_non_existing_port(self):
+        not_exists = uuidutils.generate_uuid()
+        self.assertRaises(lib_exc.NotFound,
+                          self.create_tap_mirror,
+                          port_id=not_exists,
+                          directions=self.out_direction,
+                          remote_ip=self.remote_ip,
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('123202cd-d810-4c15-bae7-26d69b24a1a4')
+    def test_multiple_mirrors_for_port(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.addCleanup(self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
+
+        # Creation of the 2nd mirror in case the direction: tunnel_id dict
+        # is different.
+        tap_mirror2 = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions={'OUT': 103},
+            remote_ip=self.remote_ip2,
+            mirror_type=self.gre
+        )
+
+        # We have a conflict if the direction: tunnel_id dict is the
+        # same
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror2['id'])
+        self.assertRaises(lib_exc.Conflict,
+                          self.create_tap_mirror,
+                          port_id=port1['id'],
+                          directions=self.out_direction,
+                          remote_ip='192.101.0.4',
+                          mirror_type=self.gre)
diff --git a/neutron_tempest_plugin/tap_as_a_service/base.py b/neutron_tempest_plugin/tap_as_a_service/base.py
index 3ddc797..99bd3cd 100644
--- a/neutron_tempest_plugin/tap_as_a_service/base.py
+++ b/neutron_tempest_plugin/tap_as_a_service/base.py
@@ -42,6 +42,14 @@
             build_interval=CONF.network.build_interval,
             build_timeout=CONF.network.build_timeout,
             **os_primary.default_params)
+        cls.tap_mirrors_client = taas_client.TapMirrorsClient(
+            os_primary.auth_provider,
+            CONF.network.catalog_type,
+            CONF.network.region or CONF.identity.region,
+            endpoint_type=CONF.network.endpoint_type,
+            build_interval=CONF.network.build_interval,
+            build_timeout=CONF.network.build_timeout,
+            **os_primary.default_params)
 
     def create_tap_service(self, **kwargs):
         body = self.tap_services_client.create_tap_service(
@@ -80,3 +88,22 @@
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.tap_flows_client.delete_tap_flow,
                         tap_flow['id'])
+
+    def create_tap_mirror(self, **kwargs):
+        body = self.tap_mirrors_client.create_tap_mirror(
+            name=data_utils.rand_name("tap_mirror"),
+            **kwargs)
+        tap_mirror = body['tap_mirror']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
+        return tap_mirror
+
+    def update_tap_mirror(self, tap_mirror_id, **kwargs):
+        body = self.tap_mirrors_client.update_tap_mirror(
+            tap_mirror_id,
+            **kwargs)
+        tap_mirror = body['tap_mirror']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py b/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
index 80389c1..d3c0d8b 100644
--- a/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
@@ -59,6 +59,14 @@
             build_interval=CONF.network.build_interval,
             build_timeout=CONF.network.build_timeout,
             **cls.os_primary.default_params)
+        cls.tap_mirrors_client = taas_client.TapMirrorsClient(
+            cls.os_primary.auth_provider,
+            CONF.network.catalog_type,
+            CONF.network.region or CONF.identity.region,
+            endpoint_type=CONF.network.endpoint_type,
+            build_interval=CONF.network.build_interval,
+            build_timeout=CONF.network.build_timeout,
+            **cls.os_primary.default_params)
 
     def _create_subnet(self, network, subnets_client=None,
                        namestart='subnet-smoke', **kwargs):
@@ -214,8 +222,10 @@
         return network, subnet, router
 
     def _create_server_with_floatingip(self, use_taas_cloud_image=False,
-                                       provider_net=False, **kwargs):
-        network = self.network
+                                       provider_net=False, network=None,
+                                       **kwargs):
+        if not network:
+            network = self.network
         if use_taas_cloud_image:
             image = CONF.neutron_plugin_options.advanced_image_ref
             flavor = CONF.neutron_plugin_options.advanced_image_flavor_ref
@@ -226,17 +236,26 @@
         if provider_net:
             network = self.provider_network
 
-        port = self.create_port(
-            network=network, security_groups=[self.secgroup['id']], **kwargs)
+        server_params = {
+            'flavor_ref': flavor,
+            'image_ref': image,
+            'key_name': self.keypair['name'],
+        }
+        if 'security_group' in kwargs:
+            server_params['security_groups'] = [
+                {'name': kwargs.pop('security_group')}]
+
+        if kwargs.get('port_security_enabled', None) is False:
+            port = self.create_port(network=network, **kwargs)
+        else:
+            port = self.create_port(
+                network=network, security_groups=[self.secgroup['id']],
+                **kwargs)
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.client.delete_port, port['id'])
 
-        params = {
-            'flavor_ref': flavor,
-            'image_ref': image,
-            'key_name': self.keypair['name']
-        }
-        vm = self.create_server(networks=[{'port': port['id']}], **params)
+        vm = self.create_server(networks=[{'port': port['id']}],
+                                **server_params)
         self.wait_for_server_active(vm['server'])
         self.wait_for_guest_os_ready(vm['server'])
 
@@ -291,3 +310,40 @@
             test_utils.call_and_ignore_notfound_exc,
             self.admin_network_client.remove_router_interface_with_subnet_id,
             self.router['id'], subnet_id=result['subnet']['id'])
+
+    def _check_icmp_traffic(self, monitor_client, left_client,
+                            left_port, right_port,
+                            tcpdump_cmd=None):
+        log_location = "/tmp/tcpdumplog"
+
+        right_ip = right_port['fixed_ips'][0]['ip_address']
+        left_ip = left_port['fixed_ips'][0]['ip_address']
+
+        # Run tcpdump in background
+        if tcpdump_cmd:
+            self._run_in_background(monitor_client, tcpdump_cmd % log_location)
+        else:
+            self._run_in_background(monitor_client,
+                                    "sudo tcpdump -n -nn > %s" % log_location)
+
+        # Ensure tcpdump is up and running
+        psax = monitor_client.exec_command("ps -ax")
+        self.assertIn("tcpdump", psax)
+
+        # Run traffic from left_vm to right_vm
+        LOG.debug('Check ICMP traffic: ping %s ', right_ip)
+        self.check_remote_connectivity(left_client, right_ip,
+                                       ping_count=50)
+
+        # Collect tcpdump results
+        output = self.monitor_client.exec_command("cat %s" % log_location)
+        self.assertLess(0, len(output))
+
+        looking_for = ["%s > %s: ICMP echo request" % (left_ip, right_ip),
+                       "%s > %s: ICMP echo reply" % (right_ip, left_ip)]
+
+        results = []
+        for tcpdump_line in looking_for:
+            results.append(tcpdump_line in output)
+
+        return all(results), output
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py b/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
new file mode 100644
index 0000000..d4a482c
--- /dev/null
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
@@ -0,0 +1,141 @@
+#    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.common import utils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+from neutron_tempest_plugin.tap_as_a_service.scenario import manager
+
+CONF = config.CONF
+
+
+class TestTapMirror(manager.BaseTaasScenarioTests):
+
+    @classmethod
+    @utils.requires_ext(extension='security-group', service='network')
+    @utils.requires_ext(extension='tap-mirror', service='network')
+    def skip_checks(cls):
+        super().skip_checks()
+
+    @classmethod
+    def resource_setup(cls):
+        super().resource_setup()
+        cls.keypair = cls.create_keypair()
+        cls.secgroup = cls.create_security_group(
+            name=data_utils.rand_name('secgroup'))
+        cls.create_loginable_secgroup_rule(secgroup_id=cls.secgroup['id'])
+        cls.create_pingable_secgroup_rule(secgroup_id=cls.secgroup['id'])
+
+    @decorators.idempotent_id('d9cfca96-fa83-417a-b111-1c02f6fe2796')
+    def test_tap_mirror_connectivity(self):
+        """Test that traffic between 2 VMs mirrored to a FIP
+
+        .. code-block:: HTML
+
+           +------------+
+           | Monitor VM |
+           |   FIP      |
+           +-----+------+
+                 |
+                 |
+           +-----+------+
+           |   NetMon   |
+           +------------+
+
+           +---------------+
+           |   Net0        |
+           +---+---------+-+
+               |         |
+               |         |
+           +---+-+     +-+---+
+           | VM0 |     | VM1 |
+           +-----+     +-----+
+
+        This is a simplified scenario adapted to the CI machinery.
+        The mirroring destination should be outside of the cloud.
+        """
+
+        # Create the topology for the 2 VMs of which the traffic
+        # will be mirrored
+        self.network, self.subnet, self.router = self.create_networks()
+
+        vm0_port, vm0_fip = self._create_server_with_floatingip(
+            security_group=self.secgroup['name']
+        )
+        vm1_port, vm1_fip = self._create_server_with_floatingip(
+            security_group=self.secgroup['name']
+        )
+        vm1_ip = vm1_port['fixed_ips'][0]['ip_address']
+
+        vm0_client = remote_client.RemoteClient(
+            vm0_fip['floating_ip_address'],
+            CONF.validation.image_ssh_user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        vm0_client.validate_authentication()
+        vm1_client = remote_client.RemoteClient(
+            vm1_fip['floating_ip_address'],
+            CONF.validation.image_ssh_user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        vm1_client.validate_authentication()
+
+        self.check_remote_connectivity(vm0_client, vm1_ip, ping_count=5)
+
+        # Create the VM which will be the destination of the mirror
+        netmon, _, _ = self.create_networks()
+        _, vm_mon_fip = self._create_server_with_floatingip(
+            use_taas_cloud_image=True, network=netmon,
+            security_group=self.secgroup['name'],
+            port_security_enabled=False,
+        )
+
+        user = CONF.neutron_plugin_options.advanced_image_ssh_user
+        self.monitor_client = remote_client.RemoteClient(
+            vm_mon_fip['floating_ip_address'], user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        self.monitor_client.validate_authentication()
+
+        r_ip = vm_mon_fip['floating_ip_address']
+        # Create GRE mirror, as tcpdump cant extract ERSPAN
+        # it is just visible as a type of GRE traffic.
+        # direction IN and that the test pings from vm0 to vm1
+        # means that ICMP echo request will be in the dump.
+        # 101 as tunnel id means that we will see 0x65 as key
+        tap_mirror = self.tap_mirrors_client.create_tap_mirror(
+            name=data_utils.rand_name("tap_mirror"),
+            port_id=vm1_port['id'],
+            directions={'IN': '101', 'OUT': '102'},
+            remote_ip=r_ip,
+            mirror_type='gre',
+        )
+        self.addCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            self.tap_mirrors_client.delete_tap_mirror,
+            tap_mirror['tap_mirror']['id']
+        )
+
+        res, output = self._check_icmp_traffic(
+            self.monitor_client,
+            vm0_client, vm0_port, vm1_port,
+            tcpdump_cmd="sudo tcpdump -vvv -n -nn proto GRE > %s")
+
+        self.assertTrue(res)
+        # GRE Key for Direction IN:101
+        self.assertIn('key=0x65', output)
+        # GRE Key for Direction OUT:102
+        self.assertIn('key=0x66', output)
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py b/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
index 904335d..6eaeb6b 100644
--- a/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
@@ -138,40 +138,6 @@
         self.right_client.validate_authentication()
         yield
 
-    def _check_icmp_traffic(self):
-        log_location = "/tmp/tcpdumplog"
-
-        right_ip = self.right_port['fixed_ips'][0]['ip_address']
-        left_ip = self.left_port['fixed_ips'][0]['ip_address']
-
-        # Run tcpdump in background
-        self._run_in_background(self.monitor_client,
-                                "sudo tcpdump -n -nn > %s" % log_location)
-
-        # Ensure tcpdump is up and running
-        psax = self.monitor_client.exec_command("ps -ax")
-        self.assertIn("tcpdump", psax)
-
-        # Run traffic from left_vm to right_vm
-        LOG.debug('Check ICMP traffic: ping %s ', right_ip)
-        # self.left_client.exec_command(
-        #     "ping -c 50 %s" % self.right_fip['floating_ip_address'])
-        self.check_remote_connectivity(self.left_client, right_ip,
-                                       ping_count=50)
-
-        # Collect tcpdump results
-        output = self.monitor_client.exec_command("cat %s" % log_location)
-        self.assertLess(0, len(output))
-
-        looking_for = ["IP %s > %s: ICMP echo request" % (left_ip, right_ip),
-                       "IP %s > %s: ICMP echo reply" % (right_ip, left_ip)]
-
-        results = []
-        for tcpdump_line in looking_for:
-            results.append(tcpdump_line in output)
-
-        return all(results)
-
     def _test_taas_connectivity(self, use_provider_net=False):
         """Ensure TAAS doesn't break connectivity
 
@@ -222,7 +188,9 @@
 
         with self._setup_topology(use_taas_cloud_image=True):
             # Check that traffic was forwarded to TAAS service
-            self.assertTrue(self._check_icmp_traffic())
+            self.assertTrue(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('6c54d9c5-075a-4a1f-bbe6-12c3c9abf1e2')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -234,7 +202,9 @@
 
         with self._setup_topology(taas=False, use_taas_cloud_image=True):
             # Check that traffic was NOT forwarded to TAAS service
-            self.assertFalse(self._check_icmp_traffic())
+            self.assertFalse(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('fcb15ca3-ef61-11e9-9792-f45c89c47e12')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -247,7 +217,9 @@
         with self._setup_topology(use_taas_cloud_image=True,
                                   provider_net=True):
             # Check that traffic was forwarded to TAAS service
-            self.assertTrue(self._check_icmp_traffic())
+            self.assertTrue(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('6c54d9c5-075a-4a1f-bbe6-12c3c9abf1e3')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -260,4 +232,6 @@
         with self._setup_topology(taas=False, use_taas_cloud_image=True,
                                   provider_net=True):
             # Check that traffic was NOT forwarded to TAAS service
-            self.assertFalse(self._check_icmp_traffic())
+            self.assertFalse(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
diff --git a/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py b/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
index 7230cbb..8998f1c 100644
--- a/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
+++ b/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
@@ -61,3 +61,27 @@
     def list_tap_flows(self, **filters):
         uri = '/taas/tap_flows'
         return self.list_resources(uri, **filters)
+
+
+class TapMirrorsClient(base.BaseNetworkClient):
+    def create_tap_mirror(self, **kwargs):
+        uri = '/taas/tap_mirrors'
+        post_data = {'tap_mirror': kwargs}
+        return self.create_resource(uri, post_data)
+
+    def update_tap_mirror(self, tap_mirror_id, **kwargs):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        post_data = {'tap_mirror': kwargs}
+        return self.update_resource(uri, post_data)
+
+    def show_tap_mirror(self, tap_mirror_id, **fields):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        return self.show_resource(uri, **fields)
+
+    def delete_tap_mirror(self, tap_mirror_id):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        return self.delete_resource(uri)
+
+    def list_tap_mirrors(self, **filters):
+        uri = '/taas/tap_mirrors'
+        return self.list_resources(uri, **filters)
diff --git a/zuul.d/2023_1_jobs.yaml b/zuul.d/2023_1_jobs.yaml
index d69054c..0bda403 100644
--- a/zuul.d/2023_1_jobs.yaml
+++ b/zuul.d/2023_1_jobs.yaml
@@ -318,5 +318,9 @@
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2023.1
     vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2024_1_jobs.yaml b/zuul.d/2024_1_jobs.yaml
index 674d1ab..65fa585 100644
--- a/zuul.d/2024_1_jobs.yaml
+++ b/zuul.d/2024_1_jobs.yaml
@@ -301,5 +301,9 @@
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.1
     vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2024_2_jobs.yaml b/zuul.d/2024_2_jobs.yaml
index e19767c..d54cf35 100644
--- a/zuul.d/2024_2_jobs.yaml
+++ b/zuul.d/2024_2_jobs.yaml
@@ -287,3 +287,10 @@
     parent: neutron-tempest-plugin-tap-as-a-service
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.2
+    vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
+      devstack_localrc:
+        NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2025_1_jobs.yaml b/zuul.d/2025_1_jobs.yaml
index 40d600f..f1e5b2b 100644
--- a/zuul.d/2025_1_jobs.yaml
+++ b/zuul.d/2025_1_jobs.yaml
@@ -252,3 +252,9 @@
     parent: neutron-tempest-plugin-tap-as-a-service
     nodeset: openstack-single-node-noble
     override-checkout: stable/2025.1
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn-2025-1
+    parent: neutron-tempest-plugin-tap-as-a-service-ovn
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.1
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 232f0a1..11e66b7 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -1512,12 +1512,14 @@
       network_api_extensions_tempest:
         - taas
         - taas-vlan-filter
+        - tap-mirror
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
         BUILD_TIMEOUT: 784
         Q_AGENT: openvswitch
         Q_ML2_TENANT_NETWORK_TYPE: vxlan,vlan
         Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch
+        OVS_BRANCH: "branch-3.3"
       devstack_local_conf:
         post-config:
           /$NEUTRON_CORE_PLUGIN_CONF:
@@ -1569,10 +1571,11 @@
         q-svc: true
         neutron: true
         taas: true
+        tap_mirror: true
         taas_openvswitch_agent: true
         tempest: true
         dstat: true
-    irrelevant-files:
+    irrelevant-files: &taas_irrelevant_files
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1604,3 +1607,52 @@
       # Ignore everything except for zuul.d/project.yaml
       - ^zuul.d/.*_jobs\.yaml$
       - ^zuul.d/base-nested-switch.yaml
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn
+    parent: neutron-tempest-plugin-base
+    description: |
+      Test tap-mirrors with OVN
+    roles:
+      - zuul: openstack/devstack
+    required-projects:
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tap-as-a-service
+      - openstack/tempest
+    vars:
+      tempest_concurrency: 4
+      tempest_test_regex: ^neutron_tempest_plugin\.tap_as_a_service
+      tox_envlist: all
+      network_api_extensions_tempest:
+        - taas
+        - tap-mirror
+      devstack_localrc:
+        Q_AGENT: ovn
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
+        BUILD_TIMEOUT: 784
+        TAAS_SERVICE_DRIVER: "TAAS:TAAS:neutron_taas.services.taas.service_drivers.ovn.taas_ovn.TaasOvnDriver:default"
+        # mirroring is available from OVN 22.12.0 and use OVS 3.2.1 that also have this
+        # feature and builds with the above OVN
+        OVN_BRANCH: "branch-24.03"
+        OVS_BRANCH: "branch-3.3"
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            neutron_plugin_options:
+              image_is_advanced: true
+              advanced_image_flavor_ref: d1
+            taas:
+              provider_physical_network: public
+              provider_segmentation_id: 100
+            image_feature_enabled:
+              api_v2: true
+      devstack_plugins:
+        neutron: git://opendev.org/openstack/neutron.git
+        neutron-tempest-plugin: https://opendev.org/openstack/neutron-tempest-plugin.git
+        tap-as-a-service: git://opendev.org/openstack/tap-as-a-service.git
+      devstack_services:
+        tap_mirror: true
+        taas: true
+        tempest: true
+    irrelevant-files: *taas_irrelevant_files
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index f9f70dd..1720745 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -235,9 +235,11 @@
         - neutron-tempest-plugin-vpnaas-2024-2
         - neutron-tempest-plugin-vpnaas-2025-1
         - neutron-tempest-plugin-tap-as-a-service
+        - neutron-tempest-plugin-tap-as-a-service-ovn
         - neutron-tempest-plugin-tap-as-a-service-2024-1
         - neutron-tempest-plugin-tap-as-a-service-2024-2
         - neutron-tempest-plugin-tap-as-a-service-2025-1
+        - neutron-tempest-plugin-tap-as-a-service-ovn-2025-1
 
     gate:
       jobs:
diff --git a/zuul.d/xena_jobs.yaml b/zuul.d/xena_jobs.yaml
index 847c611..7d58efa 100644
--- a/zuul.d/xena_jobs.yaml
+++ b/zuul.d/xena_jobs.yaml
@@ -284,5 +284,8 @@
     required-projects: *required-projects-xena
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/yoga_jobs.yaml b/zuul.d/yoga_jobs.yaml
index 2c18450..72a659e 100644
--- a/zuul.d/yoga_jobs.yaml
+++ b/zuul.d/yoga_jobs.yaml
@@ -306,5 +306,8 @@
     required-projects: *required-projects-yoga
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/zed_jobs.yaml b/zuul.d/zed_jobs.yaml
index fe98935..9c40f76 100644
--- a/zuul.d/zed_jobs.yaml
+++ b/zuul.d/zed_jobs.yaml
@@ -327,5 +327,8 @@
     required-projects: *required-projects-zed
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false