Merge "Enable tempest jobs for stable/train"
diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py
index e767298..44bf3e5 100644
--- a/octavia_tempest_plugin/common/constants.py
+++ b/octavia_tempest_plugin/common/constants.py
@@ -104,6 +104,7 @@
 HTTPS = 'HTTPS'
 TCP = 'TCP'
 TERMINATED_HTTPS = 'TERMINATED_HTTPS'
+UDP = 'UDP'
 
 # HTTP Methods
 GET = 'GET'
@@ -195,6 +196,9 @@
     STATUS_PENDING_DELETE, STATUS_DELETED, STATUS_ERROR
 )
 
+# Amphora providers list
+AMPHORA_PROVIDERS = ['amphora', 'amphorav2', 'octavia']
+
 # Flavor capabilities
 LOADBALANCER_TOPOLOGY = 'loadbalancer_topology'
 
diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py
index 6d50c89..29dc1da 100644
--- a/octavia_tempest_plugin/config.py
+++ b/octavia_tempest_plugin/config.py
@@ -108,8 +108,12 @@
                       'octavia.api.drivers entrypoint. Example: '
                       'amphora:The Octavia Amphora driver.,'
                       'octavia:Deprecated alias of the Octavia '
-                      'Amphora driver.'),
+                      'Amphora driver.,'
+                      'amphorav2:The Octavia Amphora driver that uses '
+                      'taskflow jobboard persistence.'),
                 default={'amphora': 'The Octavia Amphora driver.',
+                         'amphorav2': 'The Octavia Amphora driver that uses '
+                                      'taskflow jobboard persistence.',
                          'octavia': 'Deprecated alias of the Octavia Amphora '
                          'driver.'}),
     cfg.StrOpt('loadbalancer_topology',
@@ -173,7 +177,7 @@
                default='ubuntu',
                help='The amphora SSH user.'),
     cfg.StrOpt('amphora_ssh_key',
-               default='/tmp/octavia_ssh_key',
+               default='/etc/octavia/.ssh/octavia_ssh_key',
                help='The amphora SSH key file.'),
     # Environment specific options
     # These are used to accomodate clouds with specific limitations
@@ -184,6 +188,9 @@
     cfg.StrOpt('availability_zone',
                default=None,
                help='Availability zone to use for creating servers.'),
+    cfg.BoolOpt('test_reuse_connection', default=True,
+                help='Reuse TCP connections while testing LB with '
+                     'HTTP members (keep-alive).'),
 ]
 
 lb_feature_enabled_group = cfg.OptGroup(name='loadbalancer-feature-enabled',
diff --git a/octavia_tempest_plugin/tests/act_stdby_scenario/v2/test_active_standby_iptables.py b/octavia_tempest_plugin/tests/act_stdby_scenario/v2/test_active_standby_iptables.py
index 97886b5..8399f67 100644
--- a/octavia_tempest_plugin/tests/act_stdby_scenario/v2/test_active_standby_iptables.py
+++ b/octavia_tempest_plugin/tests/act_stdby_scenario/v2/test_active_standby_iptables.py
@@ -13,7 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import os
 import testtools
 
 from oslo_log import log as logging
@@ -50,6 +49,8 @@
             raise cls.skipException("Configured load balancer topology is not "
                                     "%s." % const.ACTIVE_STANDBY)
 
+        cls._get_amphora_ssh_key()
+
     @classmethod
     def resource_setup(cls):
         """Setup resources needed by the tests."""
@@ -230,13 +231,12 @@
     @classmethod
     def _get_amphora_ssh_key(cls):
         key_file = CONF.load_balancer.amphora_ssh_key
-        if not key_file:
-            raise Exception("SSH key file not provided.")
-        if not os.path.isfile(key_file):
-            raise Exception("Could not find amphora ssh key file {1}."
+        try:
+            with open(key_file, 'r') as f:
+                return f.read()
+        except IOError:
+            raise Exception("Could not open amphora SSH key file {0}."
                             .format(key_file))
-        with open(key_file, 'r') as f:
-            return f.read()
 
     @testtools.skipIf(CONF.load_balancer.test_with_noop,
                       'Active/Standby tests will not work in noop mode.')
@@ -273,9 +273,6 @@
         # Delete active amphora
         self.os_admin_servers_client.delete_server(active[const.COMPUTE_ID])
 
-        # Send some traffic
-        self.check_members_balanced(self.lb_vip_address)
-
         # Wait for the amphora failover to start
         waiters.wait_for_status(
             self.mem_lb_client.show_loadbalancer,
@@ -283,6 +280,9 @@
             const.PENDING_UPDATE, CONF.load_balancer.check_interval,
             CONF.load_balancer.check_timeout)
 
+        # Send some traffic (checks VRRP failover)
+        self.check_members_balanced(self.lb_vip_address)
+
         # Wait for the load balancer to return to ACTIVE
         waiters.wait_for_status(
             self.mem_lb_client.show_loadbalancer,
diff --git a/octavia_tempest_plugin/tests/api/v2/test_amphora.py b/octavia_tempest_plugin/tests/api/v2/test_amphora.py
index 7873679..7cf77dd 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_amphora.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_amphora.py
@@ -30,7 +30,7 @@
     @classmethod
     def skip_checks(cls):
         super(AmphoraAPITest, cls).skip_checks()
-        if CONF.load_balancer.provider not in ['amphora', 'octavia']:
+        if CONF.load_balancer.provider not in const.AMPHORA_PROVIDERS:
             raise cls.skipException('Amphora tests only run with the amphora '
                                     'provider enabled.')
 
diff --git a/octavia_tempest_plugin/tests/api/v2/test_listener.py b/octavia_tempest_plugin/tests/api/v2/test_listener.py
index 691c61c..3a45656 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_listener.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_listener.py
@@ -161,6 +161,140 @@
             self.assertEqual(1000, listener[const.TIMEOUT_MEMBER_DATA])
             self.assertEqual(50, listener[const.TIMEOUT_TCP_INSPECT])
 
+    @decorators.idempotent_id('cceac303-4db5-4d5a-9f6e-ff33780a5f29')
+    def test_listener_create_on_same_port(self):
+        """Tests listener creation on same port number.
+
+        * Create a first listener.
+        * Create a new listener on an existing port, but with a different
+          protocol.
+        * Create a second listener with the same parameters and ensure that
+          an error is triggered.
+        * Create a third listener with another protocol over TCP, and ensure
+          that it fails.
+        """
+
+        # Using listeners on the same port for TCP and UDP was not supported
+        # before Train. Use 2.11 API version as reference to detect previous
+        # releases and skip the test.
+        if not self.mem_listener_client.is_version_supported(
+                self.api_version, '2.11'):
+            raise self.skipException('TCP and UDP listeners on same port fix '
+                                     'is only available on Octavia API '
+                                     'version 2.11 or newer.')
+
+        listener_name = data_utils.rand_name("lb_member_listener1-create")
+
+        listener_kwargs = {
+            const.NAME: listener_name,
+            const.ADMIN_STATE_UP: True,
+            const.PROTOCOL: self.protocol,
+            const.PROTOCOL_PORT: 8080,
+            const.LOADBALANCER_ID: self.lb_id,
+            const.CONNECTION_LIMIT: 200
+        }
+
+        try:
+            listener = self.mem_listener_client.create_listener(
+                **listener_kwargs)
+        except exceptions.BadRequest as e:
+            faultstring = e.resp_body.get('faultstring', '')
+            if ("Invalid input for field/attribute protocol." in faultstring
+                    and "Value should be one of:" in faultstring):
+                raise self.skipException("Skipping unsupported protocol")
+            raise e
+
+        self.addClassResourceCleanup(
+            self.mem_listener_client.cleanup_listener,
+            listener[const.ID],
+            lb_client=self.mem_lb_client, lb_id=self.lb_id)
+
+        waiters.wait_for_status(
+            self.mem_lb_client.show_loadbalancer, self.lb_id,
+            const.PROVISIONING_STATUS, const.ACTIVE,
+            CONF.load_balancer.build_interval,
+            CONF.load_balancer.build_timeout)
+
+        if self.protocol == const.UDP:
+            protocol = const.TCP
+        else:
+            protocol = const.UDP
+
+        # Create a listener on the same port, but with a different protocol
+        listener2_name = data_utils.rand_name("lb_member_listener2-create")
+
+        listener2_kwargs = {
+            const.NAME: listener2_name,
+            const.ADMIN_STATE_UP: True,
+            const.PROTOCOL: protocol,
+            const.PROTOCOL_PORT: 8080,
+            const.LOADBALANCER_ID: self.lb_id,
+            const.CONNECTION_LIMIT: 200,
+        }
+
+        try:
+            listener2 = self.mem_listener_client.create_listener(
+                **listener2_kwargs)
+        except exceptions.BadRequest as e:
+            faultstring = e.resp_body.get('faultstring', '')
+            if ("Invalid input for field/attribute protocol." in faultstring
+                    and "Value should be one of:" in faultstring):
+                raise self.skipException("Skipping unsupported protocol")
+            raise e
+
+        self.addClassResourceCleanup(
+            self.mem_listener_client.cleanup_listener,
+            listener2[const.ID],
+            lb_client=self.mem_lb_client, lb_id=self.lb_id)
+
+        waiters.wait_for_status(
+            self.mem_lb_client.show_loadbalancer, self.lb_id,
+            const.PROVISIONING_STATUS, const.ACTIVE,
+            CONF.load_balancer.build_interval,
+            CONF.load_balancer.build_timeout)
+
+        # Create a listener on the same port, with an already used protocol
+        listener3_name = data_utils.rand_name("lb_member_listener3-create")
+
+        listener3_kwargs = {
+            const.NAME: listener3_name,
+            const.ADMIN_STATE_UP: True,
+            const.PROTOCOL: protocol,
+            const.PROTOCOL_PORT: 8080,
+            const.LOADBALANCER_ID: self.lb_id,
+            const.CONNECTION_LIMIT: 200,
+        }
+
+        self.assertRaises(
+            exceptions.Conflict,
+            self.mem_listener_client.create_listener,
+            **listener3_kwargs)
+
+        # Create a listener on the same port, with another protocol over TCP,
+        # only if layer-7 protocols are enabled
+        lb_feature_enabled = CONF.loadbalancer_feature_enabled
+        if lb_feature_enabled.l7_protocol_enabled:
+            if self.protocol == const.HTTP:
+                protocol = const.HTTPS
+            else:
+                protocol = const.HTTP
+
+            listener4_name = data_utils.rand_name("lb_member_listener4-create")
+
+            listener4_kwargs = {
+                const.NAME: listener4_name,
+                const.ADMIN_STATE_UP: True,
+                const.PROTOCOL: protocol,
+                const.PROTOCOL_PORT: 8080,
+                const.LOADBALANCER_ID: self.lb_id,
+                const.CONNECTION_LIMIT: 200,
+            }
+
+            self.assertRaises(
+                exceptions.Conflict,
+                self.mem_listener_client.create_listener,
+                **listener4_kwargs)
+
     @decorators.idempotent_id('78ba6eb0-178c-477e-9156-b6775ca7b271')
     def test_listener_list(self):
         """Tests listener list API and field filtering.
diff --git a/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
index 10ae85d..b4dde1f 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
@@ -826,7 +826,7 @@
         lb = self.mem_lb_client.show_loadbalancer(lb[const.ID])
         self.assertEqual(const.ACTIVE, lb[const.PROVISIONING_STATUS])
 
-        if CONF.load_balancer.provider in ['amphora', 'octavia']:
+        if CONF.load_balancer.provider in const.AMPHORA_PROVIDERS:
             before_amphorae = self.lb_admin_amphora_client.list_amphorae(
                 query_params='{loadbalancer_id}={lb_id}'.format(
                     loadbalancer_id=const.LOADBALANCER_ID, lb_id=lb[const.ID]))
@@ -840,7 +840,7 @@
                                      CONF.load_balancer.lb_build_interval,
                                      CONF.load_balancer.lb_build_timeout)
 
-        if CONF.load_balancer.provider in ['amphora', 'octavia']:
+        if CONF.load_balancer.provider in const.AMPHORA_PROVIDERS:
             after_amphorae = self.lb_admin_amphora_client.list_amphorae(
                 query_params='{loadbalancer_id}={lb_id}'.format(
                     loadbalancer_id=const.LOADBALANCER_ID, lb_id=lb[const.ID]))
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py b/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py
index 30a116c..9101321 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py
@@ -34,7 +34,7 @@
     def skip_checks(cls):
         super(AmphoraScenarioTest, cls).skip_checks()
 
-        if CONF.load_balancer.provider not in ['amphora', 'octavia']:
+        if CONF.load_balancer.provider not in const.AMPHORA_PROVIDERS:
             raise cls.skipException("Amphora tests require provider 'amphora' "
                                     "or 'octavia' (alias to 'amphora', "
                                     " deprecated) set")
diff --git a/octavia_tempest_plugin/tests/spare_pool_scenario/v2/test_spare_pool.py b/octavia_tempest_plugin/tests/spare_pool_scenario/v2/test_spare_pool.py
index 072bd20..80c886b 100644
--- a/octavia_tempest_plugin/tests/spare_pool_scenario/v2/test_spare_pool.py
+++ b/octavia_tempest_plugin/tests/spare_pool_scenario/v2/test_spare_pool.py
@@ -31,7 +31,7 @@
     def skip_checks(cls):
         super(SparePoolTest, cls).skip_checks()
 
-        if CONF.load_balancer.provider not in ['amphora', 'octavia']:
+        if CONF.load_balancer.provider not in const.AMPHORA_PROVIDERS:
             raise cls.skipException("Amphora tests require provider 'amphora' "
                                     "or 'octavia' (alias to 'amphora', "
                                     "deprecated) set")
diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py
index 0e8a909..c8f7954 100644
--- a/octavia_tempest_plugin/tests/test_base.py
+++ b/octavia_tempest_plugin/tests/test_base.py
@@ -862,7 +862,9 @@
 
     def check_members_balanced(self, vip_address, traffic_member_count=2,
                                protocol='http', verify=True):
-        session = requests.Session()
+        handler = requests
+        if CONF.load_balancer.test_reuse_connection:
+            handler = requests.Session()
         response_counts = {}
 
         if ipaddress.ip_address(vip_address).version == 6:
@@ -873,7 +875,7 @@
         # Send a number requests to lb vip
         for i in range(20):
             try:
-                r = session.get('{0}://{1}'.format(protocol, vip_address),
+                r = handler.get('{0}://{1}'.format(protocol, vip_address),
                                 timeout=2, verify=verify)
 
                 if r.content in response_counts:
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 3cee4b9..b9c6226 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -84,6 +84,58 @@
         octavia: https://opendev.org/openstack/octavia.git
 
 - job:
+    name: octavia-dsvm-base-ipv6-only
+    parent: devstack-tempest-ipv6
+    timeout: 7800
+    required-projects:
+      - openstack/octavia
+      - openstack/octavia-lib
+      - openstack/octavia-tempest-plugin
+      - openstack/python-octaviaclient
+    pre-run: playbooks/Octavia-DSVM/pre.yaml
+    irrelevant-files:
+      - ^.*\.rst$
+      - ^api-ref/.*$
+      - ^doc/.*$
+      - ^etc/.*$
+      - ^releasenotes/.*$
+    vars:
+      devstack_localrc:
+        TEMPEST_PLUGINS: /opt/stack/octavia-tempest-plugin
+        USE_PYTHON3: true
+      devstack_local_conf:
+        post-config:
+          $OCTAVIA_CONF:
+            DEFAULT:
+              debug: True
+      devstack_services:
+        c-bak: false
+        ceilometer-acentral: false
+        ceilometer-acompute: false
+        ceilometer-alarm-evaluator: false
+        ceilometer-alarm-notifier: false
+        ceilometer-anotification: false
+        ceilometer-api: false
+        ceilometer-collector: false
+        c-sch: false
+        c-api: false
+        c-vol: false
+        cinder: false
+        octavia: true
+        o-api: true
+        o-cw: true
+        o-hm: true
+        o-hk: true
+        swift: false
+        s-account: false
+        s-container: false
+        s-object: false
+        s-proxy: false
+        tempest: true
+      devstack_plugins:
+        octavia: https://opendev.org/openstack/octavia.git
+
+- job:
     name: octavia-dsvm-live-base
     parent: octavia-dsvm-base
     timeout: 9000
@@ -116,6 +168,38 @@
         '/var/log/octavia-tenant-traffic.log': logs
 
 - job:
+    name: octavia-dsvm-live-base-ipv6-only
+    parent: octavia-dsvm-base-ipv6-only
+    timeout: 9000
+    required-projects:
+      - openstack/diskimage-builder
+    vars:
+      devstack_localrc:
+        DIB_LOCAL_ELEMENTS: openstack-ci-mirrors
+      devstack_local_conf:
+        post-config:
+          $OCTAVIA_CONF:
+            haproxy_amphora:
+              # Set these higher for non-nested virt nodepool instances
+              connection_max_retries: 1200
+              build_active_retries: 300
+            amphora_agent:
+              forward_all_logs: True
+        test-config:
+          "$TEMPEST_CONFIG":
+            load_balancer:
+              check_interval: 1
+              check_timeout: 180
+      devstack_services:
+        neutron-qos: true
+      devstack_plugins:
+        neutron: https://opendev.org/openstack/neutron.git
+      zuul_copy_output:
+        '/var/log/dib-build' : logs
+        '/var/log/octavia-amphora.log': logs
+        '/var/log/octavia-tenant-traffic.log': logs
+
+- job:
     name: octavia-dsvm-live-two-node-base
     parent: octavia-dsvm-base
     nodeset: octavia-two-node
@@ -354,6 +438,26 @@
       - ^octavia_tempest_plugin/tests/(?!scenario/|\w+\.py).*
 
 - job:
+    name: octavia-v2-dsvm-scenario-ipv6-only
+    parent: octavia-dsvm-live-base-ipv6-only
+    vars:
+      devstack_local_conf:
+        post-config:
+          $OCTAVIA_CONF:
+            api_settings:
+              api_v1_enabled: False
+      tempest_concurrency: 2
+      tempest_test_regex: ^octavia_tempest_plugin.tests.scenario.v2
+      tox_envlist: all
+    irrelevant-files:
+      - ^.*\.rst$
+      - ^api-ref/.*$
+      - ^doc/.*$
+      - ^etc/.*$
+      - ^releasenotes/.*$
+      - ^octavia_tempest_plugin/tests/(?!scenario/|\w+\.py).*
+
+- job:
     name: octavia-v2-dsvm-py2-scenario
     parent: octavia-v2-dsvm-scenario
     vars:
@@ -623,6 +727,7 @@
             load_balancer:
               check_timeout: 180
               loadbalancer_topology: 'ACTIVE_STANDBY'
+              amphora_ssh_key: '/tmp/octavia_ssh_key'
       tempest_test_regex: ^octavia_tempest_plugin.tests.act_stdby_scenario.v2.test_active_standby_iptables
       tox_envlist: all
 
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index e50aad7..0e95165 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -20,6 +20,8 @@
         - octavia-v2-dsvm-scenario-stable-stein
         - octavia-v2-dsvm-py2-scenario-stable-rocky
         - octavia-v2-dsvm-py2-scenario-stable-queens
+        - octavia-v2-dsvm-scenario-ipv6-only:
+            voting: false
         - octavia-v2-dsvm-scenario-centos-7:
             voting: false
         - octavia-v2-dsvm-scenario-ubuntu-bionic:
@@ -75,6 +77,7 @@
         - octavia-v2-dsvm-cinder-amphora:
             voting: false
     gate:
+      fail-fast: true
       queue: octavia
       jobs:
         - octavia-v2-dsvm-noop-api