Add tests for L3 conntrack helper

API tests for L3 Conntrack Helper plugin.

Related-Bug: #1823633
Depends-On: https://review.opendev.org/670837
Change-Id: Ie085100f508f7a1cdb0fd4efbcffa1e2b485fbba
diff --git a/.zuul.yaml b/.zuul.yaml
index 44de1bf..c79e02b 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -27,6 +27,7 @@
         - dns-integration
         - empty-string-filtering
         - expose-port-forwarding-in-fip
+        - expose-l3-conntrack-helper
         - ext-gw-mode
         - external-net
         - extra_dhcp_opt
@@ -38,6 +39,7 @@
         - floating-ip-port-forwarding
         - floatingip-pools
         - ip-substring-filtering
+        - l3-conntrack-helper
         - l3-flavors
         - l3-ha
         - l3_agent_scheduler
@@ -100,6 +102,7 @@
         neutron-uplink-status-propagation: true
         neutron-network-segment-range: true
         neutron-port-forwarding: true
+        neutron-conntrack-helper: true
       devstack_local_conf:
         post-config:
           $NEUTRON_CONF:
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 71a0e5e..4441dd1 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -137,6 +137,7 @@
         cls.keypairs = []
         cls.trunks = []
         cls.network_segment_ranges = []
+        cls.conntrack_helpers = []
 
     @classmethod
     def resource_cleanup(cls):
@@ -153,6 +154,10 @@
             for floating_ip in cls.floating_ips:
                 cls._try_delete_resource(cls.delete_floatingip, floating_ip)
 
+            # Clean up conntrack helpers
+            for cth in cls.conntrack_helpers:
+                cls._try_delete_resource(cls.delete_conntrack_helper, cth)
+
             # Clean up routers
             for router in cls.routers:
                 cls._try_delete_resource(cls.delete_router,
@@ -960,6 +965,55 @@
 
         client.delete_trunk(trunk['id'])
 
+    @classmethod
+    def create_conntrack_helper(cls, router_id, helper, protocol, port,
+                                client=None):
+        """Create a conntrack helper
+
+        Create a conntrack helper and schedule it for later deletion. If a
+        client is passed, then it is used for deleteing the CTH too.
+
+        :param router_id: The ID of the Neutron router associated to the
+        conntrack helper.
+
+        :param helper: The conntrack helper module alias
+
+        :param protocol: The conntrack helper IP protocol used in the conntrack
+        helper.
+
+        :param port: The conntrack helper IP protocol port number for the
+        conntrack helper.
+
+        :param client: network client to be used for creating and cleaning up
+        the conntrack helper.
+        """
+
+        client = client or cls.client
+
+        cth = client.create_conntrack_helper(router_id, helper, protocol,
+                                             port)['conntrack_helper']
+
+        # save ID of router associated with conntrack helper for final cleanup
+        cth['router_id'] = router_id
+
+        # save client to be used later in cls.delete_conntrack_helper for final
+        # cleanup
+        cth['client'] = client
+        cls.conntrack_helpers.append(cth)
+        return cth
+
+    @classmethod
+    def delete_conntrack_helper(cls, cth, client=None):
+        """Delete conntrack helper
+
+        :param client: Client to be used
+        If client is not given it will use the client used to create the
+        conntrack helper, or cls.client if unknown.
+        """
+
+        client = client or cth.get('client') or cls.client
+        client.delete_conntrack_helper(cth['router_id'], cth['id'])
+
 
 class BaseAdminNetworkTest(BaseNetworkTest):
 
diff --git a/neutron_tempest_plugin/api/test_conntrack_helper.py b/neutron_tempest_plugin/api/test_conntrack_helper.py
new file mode 100644
index 0000000..900851a
--- /dev/null
+++ b/neutron_tempest_plugin/api/test_conntrack_helper.py
@@ -0,0 +1,135 @@
+# Copyright (c) 2019 Red Hat, Inc.
+# All rights reserved.
+#
+#    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.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions
+
+from neutron_tempest_plugin.api import base
+from neutron_tempest_plugin import config
+
+CONF = config.CONF
+
+
+class ConntrackHelperTestJSON(base.BaseNetworkTest):
+
+    required_extensions = ['router', 'l3-conntrack-helper',
+                           'expose-l3-conntrack-helper']
+
+    @classmethod
+    def resource_setup(cls):
+        super(ConntrackHelperTestJSON, cls).resource_setup()
+        cls.ext_net_id = CONF.network.public_network_id
+
+        # Create network, subnet
+        cls.network = cls.create_network()
+        cls.subnet = cls.create_subnet(cls.network)
+
+    @decorators.idempotent_id('6361c80e-902d-4c2a-88b4-ea8066507eee')
+    def test_create_list_update_show_delete_conntrack_helper(self):
+        # Create a router
+        router = self.create_router(data_utils.rand_name('router'),
+                                    external_network_id=self.ext_net_id)
+
+        # Create conntrack helper
+        created_cth = self.create_conntrack_helper(router['id'], helper='tftp',
+                                                   protocol='udp', port=69)
+        self.assertEqual('tftp', created_cth['helper'])
+        self.assertEqual('udp', created_cth['protocol'])
+        self.assertEqual(69, created_cth['port'])
+
+        # List conntrack helpers
+        conntrack_helpers = self.client.list_conntrack_helpers(
+            router['id'])['conntrack_helpers']
+        self.assertIn(created_cth['id'],
+                      {cth['id'] for cth in conntrack_helpers})
+
+        # Update conntrack helper
+        updated_conntrack_helper = self.client.update_conntrack_helper(
+            router['id'], created_cth['id'], port=6969)['conntrack_helper']
+        self.assertEqual(updated_conntrack_helper['port'], 6969)
+
+        # Show conntrack helper
+        conntrack_helper = self.client.get_conntrack_helper(
+            router['id'], created_cth['id'])['conntrack_helper']
+        self.assertEqual('tftp', conntrack_helper['helper'])
+        self.assertEqual('udp', conntrack_helper['protocol'])
+        self.assertEqual(6969, conntrack_helper['port'])
+
+        # Delete conntrack helper
+        self.client.delete_conntrack_helper(router['id'], created_cth['id'])
+        self.assertRaises(
+            exceptions.NotFound,
+            self.client.get_conntrack_helper, router['id'], created_cth['id'])
+
+    @decorators.idempotent_id('0a6ae20c-3f66-423e-93c6-cfedd1c93b8d')
+    def test_conntrack_helper_info_in_router_details(self):
+        # Create a router
+        router = self.create_router(data_utils.rand_name('router'),
+                                    external_network_id=self.ext_net_id)
+
+        # Ensure routerd does not have information about any conntrack helper
+        router = self.client.show_router(router['id'])['router']
+        self.assertEqual(0, len(router['conntrack_helpers']))
+
+        # Now create conntrack helper and ensure it's visible in Router details
+        cth = self.create_conntrack_helper(router['id'], helper='ftp',
+                                           protocol='tcp', port=21)
+        router = self.client.show_router(router['id'])['router']
+        self.assertEqual(1, len(router['conntrack_helpers']))
+        self.assertEqual('ftp', router['conntrack_helpers'][0]['helper'])
+        self.assertEqual('tcp', router['conntrack_helpers'][0]['protocol'])
+        self.assertEqual(21, router['conntrack_helpers'][0]['port'])
+
+        # Delete conntrack_helper and ensure it's no longer in Router details
+        self.client.delete_conntrack_helper(router['id'], cth['id'])
+        router = self.client.show_router(router['id'])['router']
+        self.assertEqual(0, len(router['conntrack_helpers']))
+
+    @decorators.idempotent_id('134469d9-fb25-4165-adc8-f4747f07caf1')
+    def test_2_conntrack_helpers_to_same_router(self):
+        # Create a router
+        router = self.create_router(data_utils.rand_name('router'),
+                                    external_network_id=self.ext_net_id)
+
+        cth_data = [{'helper': 'tftp', 'protocol': 'udp', 'port': 60},
+                    {'helper': 'ftp', 'protocol': 'tcp', 'port': 21}]
+        created_cths = []
+        for cth in cth_data:
+            created_cth = self.create_conntrack_helper(
+                router_id=router['id'],
+                helper=cth['helper'],
+                protocol=cth['protocol'],
+                port=cth['port'])
+            self.assertEqual(cth['helper'], created_cth['helper'])
+            self.assertEqual(cth['protocol'], created_cth['protocol'])
+            self.assertEqual(cth['port'], created_cth['port'])
+            created_cths.append(created_cth)
+
+        # Check that conntrack helpers are in Router details
+        router = self.client.show_router(router['id'])['router']
+        self.assertEqual(len(cth_data), len(router['conntrack_helpers']))
+        for cth in created_cths:
+            expected_cth = cth.copy()
+            expected_cth.pop('id')
+            expected_cth.pop('client')
+            expected_cth.pop('router_id')
+            self.assertIn(expected_cth, router['conntrack_helpers'])
+
+        # Test list of conntrack helpers
+        conntrack_helpers = self.client.list_conntrack_helpers(
+            router['id'])['conntrack_helpers']
+        self.assertEqual(len(cth_data), len(conntrack_helpers))
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index dabf3af..05095a7 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -1020,6 +1020,49 @@
         self.expected_success(204, resp.status)
         service_client.ResponseBody(resp, body)
 
+    def create_conntrack_helper(self, router_id, helper, protocol, port):
+        post_body = {'conntrack_helper': {
+            'helper': helper,
+            'protocol': protocol,
+            'port': port}}
+        body = jsonutils.dumps(post_body)
+        uri = '%s/routers/%s/conntrack_helpers' % (self.uri_prefix, router_id)
+        resp, body = self.post(uri, body)
+        self.expected_success(201, resp.status)
+        body = jsonutils.loads(body)
+        return service_client.ResponseBody(resp, body)
+
+    def get_conntrack_helper(self, router_id, cth_id):
+        uri = '%s/routers/%s/conntrack_helpers/%s' % (self.uri_prefix,
+                                                      router_id, cth_id)
+        get_resp, get_resp_body = self.get(uri)
+        self.expected_success(200, get_resp.status)
+        body = jsonutils.loads(get_resp_body)
+        return service_client.ResponseBody(get_resp, body)
+
+    def list_conntrack_helpers(self, router_id):
+        uri = '%s/routers/%s/conntrack_helpers' % (self.uri_prefix, router_id)
+        resp, body = self.get(uri)
+        self.expected_success(200, resp.status)
+        body = jsonutils.loads(body)
+        return service_client.ResponseBody(resp, body)
+
+    def update_conntrack_helper(self, router_id, cth_id, **kwargs):
+        uri = '%s/routers/%s/conntrack_helpers/%s' % (self.uri_prefix,
+                                                      router_id, cth_id)
+        put_body = jsonutils.dumps({'conntrack_helper': kwargs})
+        put_resp, resp_body = self.put(uri, put_body)
+        self.expected_success(200, put_resp.status)
+        body = jsonutils.loads(resp_body)
+        return service_client.ResponseBody(put_resp, body)
+
+    def delete_conntrack_helper(self, router_id, cth_id):
+        uri = '%s/routers/%s/conntrack_helpers/%s' % (self.uri_prefix,
+                                                      router_id, cth_id)
+        resp, body = self.delete(uri)
+        self.expected_success(204, resp.status)
+        service_client.ResponseBody(resp, body)
+
     def create_network_keystone_v3(self, name, project_id, tenant_id=None):
         uri = '%s/networks' % self.uri_prefix
         post_data = {