Merge "Remove "active" attribute from the allowed_address_pairs"
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 216ccfc..024fe43 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -784,6 +784,15 @@
         return qos_rule
 
     @classmethod
+    def create_qos_dscp_marking_rule(cls, policy_id, dscp_mark):
+        """Wrapper utility that creates and returns a QoS dscp rule."""
+        body = cls.admin_client.create_dscp_marking_rule(
+            policy_id, dscp_mark)
+        qos_rule = body['dscp_marking_rule']
+        cls.qos_rules.append(qos_rule)
+        return qos_rule
+
+    @classmethod
     def delete_router(cls, router, client=None):
         client = client or cls.client
         if 'routes' in router:
diff --git a/neutron_tempest_plugin/api/test_qos_negative.py b/neutron_tempest_plugin/api/test_qos_negative.py
index f4d6636..2d06d11 100644
--- a/neutron_tempest_plugin/api/test_qos_negative.py
+++ b/neutron_tempest_plugin/api/test_qos_negative.py
@@ -90,42 +90,122 @@
                           self.admin_client.delete_qos_policy, non_exist_id)
 
 
-class QosBandwidthLimitRuleNegativeTestJSON(base.BaseAdminNetworkTest):
+class QosRuleNegativeBaseTestJSON(base.BaseAdminNetworkTest):
 
     required_extensions = [qos_apidef.ALIAS]
 
-    @decorators.attr(type='negative')
-    @decorators.idempotent_id('e9ce8042-c828-4cb9-b1f1-85bd35e6553a')
-    def test_rule_update_rule_nonexistent_policy(self):
+    def _test_rule_update_rule_nonexistent_policy(self, create_params,
+                                                  update_params):
         non_exist_id = data_utils.rand_name('qos_policy')
         policy = self.create_qos_policy(name='test-policy',
                                         description='test policy',
                                         shared=False)
-        rule = self.create_qos_bandwidth_limit_rule(policy_id=policy['id'],
-                                                    max_kbps=1,
-                                                    max_burst_kbps=1)
+        rule = self.rule_create_m(policy_id=policy['id'], **create_params)
         self.assertRaises(
             lib_exc.NotFound,
-            self.admin_client.update_bandwidth_limit_rule,
-            non_exist_id, rule['id'], max_kbps=200, max_burst_kbps=1337)
+            self.rule_update_m,
+            non_exist_id, rule['id'], **update_params)
 
-    @decorators.attr(type='negative')
-    @decorators.idempotent_id('1b592566-745f-4e15-a439-073afe341244')
-    def test_rule_create_rule_non_existent_policy(self):
+    def _test_rule_create_rule_non_existent_policy(self, create_params):
         non_exist_id = data_utils.rand_name('qos_policy')
         self.assertRaises(
             lib_exc.NotFound,
-            self.admin_client.create_bandwidth_limit_rule,
-            non_exist_id, max_kbps=200, max_burst_kbps=300)
+            self.rule_create_m,
+            non_exist_id, **create_params)
 
-    @decorators.attr(type='negative')
-    @decorators.idempotent_id('a2c72066-0c32-4f28-be7f-78fa721588b6')
-    def test_rule_update_rule_nonexistent_rule(self):
+    def _test_rule_update_rule_nonexistent_rule(self, update_params):
         non_exist_id = data_utils.rand_name('qos_rule')
         policy = self.create_qos_policy(name='test-policy',
                                         description='test policy',
                                         shared=False)
         self.assertRaises(
             lib_exc.NotFound,
-            self.admin_client.update_bandwidth_limit_rule,
-            policy['id'], non_exist_id, max_kbps=200, max_burst_kbps=1337)
+            self.rule_update_m,
+            policy['id'], non_exist_id, **update_params)
+
+
+class QosBandwidthLimitRuleNegativeTestJSON(QosRuleNegativeBaseTestJSON):
+
+    @classmethod
+    def resource_setup(cls):
+        cls.rule_create_m = cls.create_qos_bandwidth_limit_rule
+        cls.rule_update_m = cls.admin_client.update_bandwidth_limit_rule
+        super(QosBandwidthLimitRuleNegativeTestJSON, cls).resource_setup()
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('e9ce8042-c828-4cb9-b1f1-85bd35e6553a')
+    def test_rule_update_rule_nonexistent_policy(self):
+        create_params = {'max_kbps': 1, 'max_burst_kbps': 1}
+        update_params = {'max_kbps': 200, 'max_burst_kbps': 1337}
+        self._test_rule_update_rule_nonexistent_policy(
+            create_params, update_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('1b592566-745f-4e15-a439-073afe341244')
+    def test_rule_create_rule_non_existent_policy(self):
+        create_params = {'max_kbps': 200, 'max_burst_kbps': 300}
+        self._test_rule_create_rule_non_existent_policy(create_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('a2c72066-0c32-4f28-be7f-78fa721588b6')
+    def test_rule_update_rule_nonexistent_rule(self):
+        update_params = {'max_kbps': 200, 'max_burst_kbps': 1337}
+        self._test_rule_update_rule_nonexistent_rule(update_params)
+
+
+class QosMinimumBandwidthRuleNegativeTestJSON(QosRuleNegativeBaseTestJSON):
+
+    @classmethod
+    def resource_setup(cls):
+        cls.rule_create_m = cls.create_qos_minimum_bandwidth_rule
+        cls.rule_update_m = cls.admin_client.update_minimum_bandwidth_rule
+        super(QosMinimumBandwidthRuleNegativeTestJSON, cls).resource_setup()
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('08b8455b-4d4f-4119-bad3-9357085c3a80')
+    def test_rule_update_rule_nonexistent_policy(self):
+        create_params = {'min_kbps': 1}
+        update_params = {'min_kbps': 200}
+        self._test_rule_update_rule_nonexistent_policy(
+            create_params, update_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('5a714a4a-bfbc-4cf9-b0c0-13fd185204f7')
+    def test_rule_create_rule_non_existent_policy(self):
+        create_params = {'min_kbps': 200}
+        self._test_rule_create_rule_non_existent_policy(create_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('8470cbe0-8ca5-46ab-9c66-7cf69301b121')
+    def test_rule_update_rule_nonexistent_rule(self):
+        update_params = {'min_kbps': 200}
+        self._test_rule_update_rule_nonexistent_rule(update_params)
+
+
+class QosDscpRuleNegativeTestJSON(QosRuleNegativeBaseTestJSON):
+
+    @classmethod
+    def resource_setup(cls):
+        cls.rule_create_m = cls.create_qos_dscp_marking_rule
+        cls.rule_update_m = cls.admin_client.update_dscp_marking_rule
+        super(QosDscpRuleNegativeTestJSON, cls).resource_setup()
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('d47d5fbe-3e98-476f-b2fd-97818175dea5')
+    def test_rule_update_rule_nonexistent_policy(self):
+        create_params = {'dscp_mark': 26}
+        update_params = {'dscp_mark': 16}
+        self._test_rule_update_rule_nonexistent_policy(
+            create_params, update_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('07d17f09-3dc4-4c24-9bb1-49081a153c5a')
+    def test_rule_create_rule_non_existent_policy(self):
+        create_params = {'dscp_mark': 16}
+        self._test_rule_create_rule_non_existent_policy(create_params)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('9c0bd085-5a7a-496f-a984-50dc631a64f2')
+    def test_rule_update_rule_nonexistent_rule(self):
+        update_params = {'dscp_mark': 16}
+        self._test_rule_update_rule_nonexistent_rule(update_params)
diff --git a/neutron_tempest_plugin/common/utils.py b/neutron_tempest_plugin/common/utils.py
index f03762c..1526ecf 100644
--- a/neutron_tempest_plugin/common/utils.py
+++ b/neutron_tempest_plugin/common/utils.py
@@ -136,3 +136,74 @@
 def call_url_remote(ssh_client, url):
     cmd = "curl %s --retry 3 --connect-timeout 2" % url
     return ssh_client.exec_command(cmd)
+
+
+class StatefulConnection:
+    """Class to test connection that should remain opened
+
+    Can be used to perform some actions while the initiated connection
+    remain opened
+    """
+
+    def __init__(self, client_ssh, server_ssh, target_ip, target_port):
+        self.client_ssh = client_ssh
+        self.server_ssh = server_ssh
+        self.ip = target_ip
+        self.port = target_port
+        self.connection_started = False
+        self.test_attempt = 0
+
+    def __enter__(self):
+        return self
+
+    @property
+    def test_str(self):
+        return 'attempt_{}'.format(str(self.test_attempt).zfill(3))
+
+    def _start_connection(self):
+        self.server_ssh.exec_command(
+                'echo "{}" > input.txt'.format(self.test_str))
+        self.server_ssh.exec_command('tail -f input.txt | nc -lp '
+                '{} &> output.txt &'.format(self.port))
+        self.client_ssh.exec_command(
+                'echo "{}" > input.txt'.format(self.test_str))
+        self.client_ssh.exec_command('tail -f input.txt | nc {} {} &>'
+                'output.txt &'.format(self.ip, self.port))
+
+    def _test_connection(self):
+        if not self.connection_started:
+            self._start_connection()
+        else:
+            self.server_ssh.exec_command(
+                    'echo "{}" >> input.txt'.format(self.test_str))
+            self.client_ssh.exec_command(
+                    'echo "{}" >> input.txt & sleep 1'.format(self.test_str))
+        try:
+            self.server_ssh.exec_command(
+                    'grep {} output.txt'.format(self.test_str))
+            self.client_ssh.exec_command(
+                    'grep {} output.txt'.format(self.test_str))
+            if not self.should_pass:
+                return False
+            else:
+                if not self.connection_started:
+                    self.connection_started = True
+                return True
+        except exceptions.SSHExecCommandFailed:
+            if self.should_pass:
+                return False
+            else:
+                return True
+        finally:
+            self.test_attempt += 1
+
+    def test_connection(self, should_pass=True, timeout=10, sleep_timer=1):
+        self.should_pass = should_pass
+        wait_until_true(
+                self._test_connection, timeout=timeout, sleep=sleep_timer)
+
+    def __exit__(self, type, value, traceback):
+        self.server_ssh.exec_command('sudo killall nc || killall nc')
+        self.server_ssh.exec_command('sudo killall tail || killall tail')
+        self.client_ssh.exec_command('sudo killall nc || killall nc')
+        self.client_ssh.exec_command('sudo killall tail || killall tail')
diff --git a/neutron_tempest_plugin/scenario/test_security_groups.py b/neutron_tempest_plugin/scenario/test_security_groups.py
index 8b7098e..e574a1b 100644
--- a/neutron_tempest_plugin/scenario/test_security_groups.py
+++ b/neutron_tempest_plugin/scenario/test_security_groups.py
@@ -277,6 +277,50 @@
                       'remote_ip_prefix': cidr}]
         self._test_ip_prefix(rule_list, should_succeed=False)
 
+    @decorators.idempotent_id('01f0ddca-b049-47eb-befd-82acb502c9ec')
+    def test_established_tcp_session_after_re_attachinging_sg(self):
+        """Test existing connection remain open after sg has been re-attached
+
+        Verifies that new packets can pass over the existing connection when
+        the security group has been removed from the server and then added
+        back
+        """
+
+        ssh_sg = self.create_security_group()
+        self.create_loginable_secgroup_rule(secgroup_id=ssh_sg['id'])
+        vm_ssh, fips, vms = self.create_vm_testing_sec_grp(
+                security_groups=[{'name': ssh_sg['name']}])
+        sg = self.create_security_group()
+        nc_rule = [{'protocol': constants.PROTO_NUM_TCP,
+                    'direction': constants.INGRESS_DIRECTION,
+                    'port_range_min': 6666,
+                    'port_range_max': 6666}]
+        self.create_secgroup_rules(nc_rule, secgroup_id=sg['id'])
+        srv_port = self.client.list_ports(network_id=self.network['id'],
+                device_id=vms[1]['server']['id'])['ports'][0]
+        srv_ip = srv_port['fixed_ips'][0]['ip_address']
+        with utils.StatefulConnection(
+                vm_ssh[0], vm_ssh[1], srv_ip, 6666) as con:
+            self.client.update_port(srv_port['id'],
+                    security_groups=[ssh_sg['id'], sg['id']])
+            con.test_connection()
+        with utils.StatefulConnection(
+                vm_ssh[0], vm_ssh[1], srv_ip, 6666) as con:
+            self.client.update_port(
+                    srv_port['id'], security_groups=[ssh_sg['id']])
+            con.test_connection(should_pass=False)
+        with utils.StatefulConnection(
+                vm_ssh[0], vm_ssh[1], srv_ip, 6666) as con:
+            self.client.update_port(srv_port['id'],
+                    security_groups=[ssh_sg['id'], sg['id']])
+            con.test_connection()
+            self.client.update_port(srv_port['id'],
+                    security_groups=[ssh_sg['id']])
+            con.test_connection(should_pass=False)
+            self.client.update_port(srv_port['id'],
+                    security_groups=[ssh_sg['id'], sg['id']])
+            con.test_connection()
+
     @decorators.idempotent_id('7ed39b86-006d-40fb-887a-ae46693dabc9')
     def test_remote_group(self):
         # create a new sec group
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index fd256e1..4d4b152 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -220,7 +220,11 @@
       network_available_features: *available_features
       # TODO(slaweq): remove trunks subport_connectivity test from blacklist
       # when bug https://bugs.launchpad.net/neutron/+bug/1838760 will be fixed
-      tempest_exclude_regex: "(^neutron_tempest_plugin.scenario.test_trunk.TrunkTest.test_subport_connectivity)"
+      # TODO(akatz): remove established tcp session verification test when the
+      # bug https://bugzilla.redhat.com/show_bug.cgi?id=1965036 will be fixed
+      tempest_exclude_regex: "\
+          (^neutron_tempest_plugin.scenario.test_trunk.TrunkTest.test_subport_connectivity)|\
+          (^neutron_tempest_plugin.scenario.test_security_groups.NetworkSecGroupTest.test_established_tcp_session_after_re_attachinging_sg)"
       devstack_localrc:
         Q_AGENT: openvswitch
         Q_ML2_TENANT_NETWORK_TYPE: vxlan